Files

1238 lines
36 KiB
Python

# coding: utf-8
import logging
import os
import pickle
import platform
import random
import re
import sys
import time
import pytest
import ray
import ray.cluster_utils
from ray._common.test_utils import (
SignalActor,
run_string_as_driver,
)
from ray._private.test_utils import client_test_enabled
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
import psutil
logger = logging.getLogger(__name__)
# https://github.com/ray-project/ray/issues/6662
@pytest.mark.skipif(
os.environ.get("RAY_MINIMAL") == "1",
reason="This test is not supposed to work for minimal installation.",
)
@pytest.mark.skipif(client_test_enabled(), reason="interferes with grpc")
def test_http_proxy(start_http_proxy, shutdown_only):
# C++ config `grpc_enable_http_proxy` only initializes once, so we have to
# run driver as a separate process to make sure the correct config value
# is initialized.
script = """
import ray
ray.init(num_cpus=1)
@ray.remote
def f():
return 1
assert ray.get(f.remote()) == 1
"""
env = start_http_proxy
run_string_as_driver(script, dict(os.environ, **env))
def test_release_cpu_resources(shutdown_only):
ray.init(num_cpus=1)
@ray.remote(num_cpus=1)
def child():
return 3
@ray.remote(num_cpus=1)
def parent():
# Parent should release the CPU resource
# to run child.
return ray.get(child.remote())
assert ray.get(parent.remote()) == 3
# Make sure CPU resource inside PG can also be released properly.
pg = ray.util.placement_group(bundles=[{"CPU": 1}])
assert (
ray.get(
parent.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=pg, placement_group_capture_child_tasks=True
)
).remote()
)
== 3
)
assert (
ray.get(
parent.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=pg,
placement_group_bundle_index=0,
placement_group_capture_child_tasks=True,
)
).remote()
)
== 3
)
# https://github.com/ray-project/ray/issues/16025
def test_release_resources_race(shutdown_only):
ray.init(num_cpus=2)
refs = []
for _ in range(10):
refs.append(ray.put(bytearray(1024 * 1024)))
@ray.remote
def consume(refs):
# Should work without releasing resources!
ray.get(refs)
return os.getpid()
pids = set(ray.get([consume.remote(refs) for _ in range(10)]))
# Should not have started multiple workers.
assert len(pids) <= 2, pids
def test_not_release_resource(shutdown_only):
# Test to make sure we don't release CPU
# resource if the object is already fetched.
ray.init(num_cpus=1)
@ray.remote
def task1():
return [1] * (1024 * 1024)
o1 = task1.remote()
@ray.remote
def task2(*args, **kwargs):
# ray.get here should not release
# CPU resource since the object is already
# available in args[0]
assert args[0] == ray.get(kwargs["o"][0])
return os.getpid()
@ray.remote
def task3(*args):
return os.getpid()
o2 = task2.remote(o1, o=[o1])
# This should run after task2 finishes
o3 = task3.remote(o1)
assert len(set(ray.get([o2, o3]))) == 1
# https://github.com/ray-project/ray/issues/22504
def test_worker_isolation_by_resources(shutdown_only):
ray.init(num_cpus=1, num_gpus=1)
@ray.remote(num_gpus=1)
def gpu():
return os.getpid()
@ray.remote
def cpu():
return os.getpid()
pid1 = ray.get(cpu.remote())
pid2 = ray.get(gpu.remote())
assert pid1 != pid2, (pid1, pid2)
# https://github.com/ray-project/ray/issues/10960
def test_max_calls_releases_resources(shutdown_only):
ray.init(num_cpus=2, num_gpus=1)
@ray.remote(num_cpus=0)
def g():
return 0
@ray.remote(num_cpus=1, num_gpus=1, max_calls=1, max_retries=0)
def f():
return [g.remote()]
for i in range(10):
print(i)
ray.get(f.remote()) # This will hang if GPU resources aren't released.
# https://github.com/ray-project/ray/issues/7263
def test_grpc_message_size(shutdown_only):
ray.init(num_cpus=1)
@ray.remote
def bar(*a):
return
# 50KiB, not enough to spill to plasma, but will be inlined.
def f():
return bytearray(50000)
# Executes a 10MiB task spec
ray.get(bar.remote(*[f() for _ in range(200)]))
def test_default_worker_import_dependency(shutdown_only):
"""
Test ray's python worker import doesn't import the not-allowed dependencies.
"""
# We don't allow numpy to be imported in the worker script to avoid slow
# worker startup time, as well as interfering with OMP_NUM_THREADS which
# is used by numpy when imported.
# See https://github.com/ray-project/ray/issues/33891
blocked_deps = ["numpy"]
# Ray should not be importing pydantic (used in serialization) eagerly.
# This introduces regression in worker start up time.
# https://github.com/ray-project/ray/issues/41338
blocked_deps += ["pydantic"]
# Remove the ray module and the blocked deps from sys.modules.
sys.modules.pop("ray", None)
assert "ray" not in sys.modules
for dep in blocked_deps:
sys.modules.pop(dep, None)
assert dep not in sys.modules
# This imports the python worker.
import ray._private.workers.default_worker # noqa: F401
# Check that the ray module is imported.
assert "ray" in sys.modules
# Check that the blocked deps are not imported.
for dep in blocked_deps:
assert dep not in sys.modules
# Test starting a ray workers should not see unwanted deps loaded eagerly.
ray.init()
@ray.remote
def f():
import ray # noqa: F401
assert "ray" in sys.modules
for x in blocked_deps:
assert x not in sys.modules
ray.get(f.remote())
@pytest.mark.skipif(
sys.platform != "linux", reason="Windows/OSX thread count not policed yet."
)
@pytest.mark.parametrize(
"recorder_env, expected_thread_counts",
[
pytest.param(
{"RAY_enable_ray_task_event_recorder": "0"},
{21, 22, 23, 24},
id="task_event_recorder_off",
),
pytest.param(
{"RAY_enable_ray_event": "1", "RAY_enable_ray_task_event_recorder": "1"},
{22, 23, 24, 25},
id="task_event_recorder_on",
),
],
)
def test_worker_thread_count(
monkeypatch, shutdown_only, recorder_env, expected_thread_counts
):
"""This test will fail if the number of threads spawned by a worker process
increases. If you find that a patch is now causing this test to fail,
consider if this thread count change is expected and adjust the test
(or your patch) accordingly!
RayTaskEventRecorder spawns one dedicated io thread when active (which needs both
enable_ray_event and enable_ray_task_event_recorder), so the expected count is
parametrized on that flag instead of depending on the compiled default.
"""
@ray.remote
class Actor:
def get_thread_count(self):
try:
process = psutil.Process(os.getpid())
return process.num_threads()
except ImportError:
return None
# Set the environment variables used by the raylet and worker
monkeypatch.setenv("RAY_worker_num_grpc_internal_threads", "1")
monkeypatch.setenv("RAY_num_server_call_thread", "1")
monkeypatch.setenv("RAY_core_worker_num_server_call_thread", "1")
for k, v in recorder_env.items():
monkeypatch.setenv(k, v)
# TODO(#55215): The for loop and the 'assert ... in {..,..}' complicates this
# test unnecessarily. We should only need to call the assert after
# a single call to the worker. However, because the thread count
# per worker today isn't entirely static, we need to allow for this
# flexibility. https://github.com/ray-project/ray/issues/55215
actor = Actor.remote()
for _ in range(5):
ray.get(actor.get_thread_count.remote())
# Lowering these numbers in this assert should be celebrated,
# increasing these numbers should be scrutinized
assert ray.get(actor.get_thread_count.remote()) in expected_thread_counts
# https://github.com/ray-project/ray/issues/7287
def test_omp_threads_set(ray_start_cluster, monkeypatch):
cluster = ray_start_cluster
cluster.add_node(num_cpus=2)
ray.init(address=cluster.address)
@ray.remote
def f():
return os.environ.get("OMP_NUM_THREADS")
@ray.remote
class Actor:
def f(self):
return os.environ.get("OMP_NUM_THREADS")
###########################
# Test basic tasks
###########################
# Test override to num_cpus if OMP_NUM_THREADS not set
assert ray.get(f.options(num_cpus=2).remote()) == "2"
# Test override to default cpu number if OMP_NUM_THREADS not set
assert ray.get(f.remote()) == "1"
# Test set to 1 for fractional CPU
assert ray.get(f.options(num_cpus=0.25).remote()) == "1"
###########################
# Test not overriding env_variables
###########################
from ray.runtime_env import RuntimeEnv
assert (
ray.get(
f.options(
runtime_env=RuntimeEnv(env_vars={"OMP_NUM_THREADS": "2"})
).remote()
)
== "2"
)
assert (
ray.get(
f.options(
num_cpus=1, runtime_env=RuntimeEnv(env_vars={"OMP_NUM_THREADS": "2"})
).remote()
)
== "2"
)
###########################
# Test actor tasks
###########################
# Test actor tasks set OMP_NUM_THREADS correctly in a similar way.
assert ray.get(Actor.remote().f.remote()) == "1"
assert ray.get(Actor.options(num_cpus=2).remote().f.remote()) == "2"
assert ray.get(Actor.options(num_cpus=0.25).remote().f.remote()) == "1"
###########################
# Test setting and restoring of the environ after tasks run
###########################
@ray.remote
def g():
return os.getpid(), os.environ.get("OMP_NUM_THREADS")
# Set to 1
pid1, omp_num_threads = ray.get(g.remote())
assert omp_num_threads == "1"
# Set to 2
pid2, omp_num_threads = ray.get(g.options(num_cpus=2).remote())
assert pid1 == pid2
assert omp_num_threads == "2"
###########################
# Test not setting the value with environ already set to 1 in env
###########################
with monkeypatch.context() as m:
m.setenv("OMP_NUM_THREADS", "1")
cluster.add_node(num_cpus=4)
assert ray.get(f.options(num_cpus=4).remote()) == "1"
def test_submit_api(shutdown_only):
ray.init(num_cpus=2, num_gpus=1, resources={"Custom": 1})
@ray.remote
def f(n):
return list(range(n))
@ray.remote
def g():
return ray.get_gpu_ids()
assert f._remote([0], num_returns=0) is None
id1 = f._remote(args=[1], num_returns=1)
assert ray.get(id1) == [0]
id1, id2 = f._remote(args=[2], num_returns=2)
assert ray.get([id1, id2]) == [0, 1]
id1, id2, id3 = f._remote(args=[3], num_returns=3)
assert ray.get([id1, id2, id3]) == [0, 1, 2]
assert ray.get(
g._remote(args=[], num_cpus=1, num_gpus=1, resources={"Custom": 1})
) == [0]
infeasible_id = g._remote(args=[], resources={"NonexistentCustom": 1})
assert ray.get(g._remote()) == []
ready_ids, remaining_ids = ray.wait([infeasible_id], timeout=0.05)
assert len(ready_ids) == 0
assert len(remaining_ids) == 1
# Check mismatch with num_returns.
with pytest.raises(ValueError):
ray.get(f.options(num_returns=2).remote(3))
with pytest.raises(ValueError):
ray.get(f.options(num_returns=3).remote(2))
@ray.remote
class Actor:
def __init__(self, x, y=0):
self.x = x
self.y = y
def method(self, a, b=0):
return self.x, self.y, a, b
def gpu_ids(self):
return ray.get_gpu_ids()
@ray.remote
class Actor2:
def __init__(self):
pass
def method(self):
pass
a = Actor._remote(args=[0], kwargs={"y": 1}, num_gpus=1, resources={"Custom": 1})
a2 = Actor2._remote()
ray.get(a2.method._remote())
id1, id2, id3, id4 = a.method._remote(args=["test"], kwargs={"b": 2}, num_returns=4)
assert ray.get([id1, id2, id3, id4]) == [0, 1, "test", 2]
def test_invalid_arguments():
def f():
return 1
class A:
x = 1
template1 = (
"The type of keyword '{}' "
+ f"must be {(int, type(None))}, but received type {float}"
)
# Type check
for keyword in ("max_retries", "max_calls"):
with pytest.raises(TypeError, match=re.escape(template1.format(keyword))):
ray.remote(**{keyword: random.random()})(f)
num_returns_template = (
"The type of keyword 'num_returns' "
+ f"must be {(int, str, type(None))}, but received type {float}"
)
with pytest.raises(TypeError, match=re.escape(num_returns_template)):
ray.remote(**{"num_returns": random.random()})(f)
for keyword in ("max_restarts", "max_task_retries"):
with pytest.raises(TypeError, match=re.escape(template1.format(keyword))):
ray.remote(**{keyword: random.random()})(A)
# Value check for non-negative finite values
for v in (random.randint(-100, -2), -1):
keyword = "max_calls"
with pytest.raises(
ValueError,
match=f"The keyword '{keyword}' only accepts None, "
f"0 or a positive integer",
):
ray.remote(**{keyword: v})(f)
keyword = "num_returns"
with pytest.raises(
ValueError,
match=f"The keyword '{keyword}' only accepts None, "
"a non-negative integer, "
r"'streaming' \(for generators\), or 'dynamic'",
):
ray.remote(**{keyword: v})(f)
# Value check for non-negative and infinite values
template2 = (
"The keyword '{}' only accepts None, 0, -1 or a positive integer, "
"where -1 represents infinity."
)
with pytest.raises(ValueError, match=template2.format("max_retries")):
ray.remote(max_retries=random.randint(-100, -2))(f)
for keyword in ("max_restarts", "max_task_retries"):
with pytest.raises(ValueError, match=template2.format(keyword)):
ray.remote(**{keyword: random.randint(-100, -2)})(A)
# Check invalid resource quantity
with pytest.raises(
ValueError,
match=(
"The precision of the fractional quantity of resource num_gpus"
" cannot go beyond 0.0001"
),
):
ray.remote(num_gpus=0.0000001)(f)
with pytest.raises(
ValueError,
match=(
"The precision of the fractional quantity of resource custom_resource"
" cannot go beyond 0.0001"
),
):
ray.remote(resources={"custom_resource": 0.0000001})(f)
def test_options():
"""General test of option keywords in Ray."""
from ray._common import ray_option_utils
def f():
return 1
class A:
x = 1
task_defaults = {
k: v.default_value for k, v in ray_option_utils.task_options.items()
}
task_defaults_for_options = task_defaults.copy()
task_defaults_for_options.pop("max_calls")
ray.remote(f).options(**task_defaults_for_options)
ray.remote(**task_defaults)(f).options(**task_defaults_for_options)
with pytest.raises(
ValueError,
match=re.escape("Setting 'max_calls' is not supported in '.options()'."),
):
ray.remote(f).options(max_calls=1)
actor_defaults = {
k: v.default_value for k, v in ray_option_utils.actor_options.items()
}
actor_defaults_for_options = actor_defaults.copy()
actor_defaults_for_options.pop("concurrency_groups")
ray.remote(A).options(**actor_defaults_for_options)
ray.remote(**actor_defaults)(A).options(**actor_defaults_for_options)
with pytest.raises(
ValueError,
match=re.escape(
"Setting 'concurrency_groups' is not supported in '.options()'."
),
):
ray.remote(A).options(concurrency_groups=[])
unique_object = type("###", (), {})()
for k, v in ray_option_utils.task_options.items():
v.validate(k, v.default_value)
with pytest.raises(TypeError):
v.validate(k, unique_object)
for k, v in ray_option_utils.actor_options.items():
v.validate(k, v.default_value)
with pytest.raises(TypeError):
v.validate(k, unique_object)
@ray.remote(num_gpus=2)
def foo():
pass
assert foo._default_options == {
"max_calls": 1,
"num_gpus": 2,
}
f2 = foo.options(num_cpus=1, num_gpus=1)
# TODO(suquark): The current implementation of `.options()` is so bad that we
# cannot even access its options from outside. Here we hack the closures to
# achieve our goal. Need futher efforts to clean up the tech debt.
assert f2.remote.__closure__[2].cell_contents == {
"num_cpus": 1,
"num_gpus": 1,
}
# https://github.com/ray-project/ray/issues/17842
def test_disable_cuda_devices():
script = """
import ray
ray.init()
@ray.remote
def check():
import os
assert "CUDA_VISIBLE_DEVICES" not in os.environ
print("remote", ray.get(check.remote()))
"""
run_string_as_driver(
script, dict(os.environ, **{"RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES": "1"})
)
# https://github.com/ray-project/ray/issues/54868
@pytest.mark.skipif(
sys.platform == "darwin" and platform.machine() == "arm64",
reason=(
"On Apple Silicon the GPU resource is managed by AppleGPUAcceleratorManager, "
"which has no visible-devices env var (e.g. CUDA_VISIBLE_DEVICES) to set or "
"override, so this NVIDIA-specific override behavior does not apply. Manager "
"selection is hardware-based, so NVIDIA semantics can't be exercised on a Mac."
),
)
def test_not_override_accelerator_ids_when_num_accelerators_is_zero():
not_override_check_script = """
import ray
ray.init()
@ray.remote(num_gpus=0)
def check():
import os
assert "CUDA_VISIBLE_DEVICES" not in os.environ
@ray.remote(num_gpus=0)
class Actor:
def check(self):
import os
assert "CUDA_VISIBLE_DEVICES" not in os.environ
print("task check", ray.get(check.remote()))
print("actor check", ray.get(Actor.options(num_gpus=0).remote().check.remote()))
"""
run_string_as_driver(not_override_check_script)
override_check_script = """
import ray
ray.init()
@ray.remote(num_gpus=0)
def check():
import os
assert os.environ.get("CUDA_VISIBLE_DEVICES") == ""
@ray.remote(num_gpus=0)
class Actor:
def check(self):
import os
assert os.environ.get("CUDA_VISIBLE_DEVICES") == ""
print("task check", ray.get(check.remote()))
print("actor check", ray.get(Actor.options(num_gpus=0).remote().check.remote()))
"""
run_string_as_driver(
override_check_script,
dict(
os.environ,
**{"RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO": "1"},
),
)
def test_put_get(shutdown_only):
ray.init(num_cpus=0)
for i in range(100):
value_before = i * 10**6
object_ref = ray.put(value_before)
value_after = ray.get(object_ref)
assert value_before == value_after
for i in range(100):
value_before = i * 10**6 * 1.0
object_ref = ray.put(value_before)
value_after = ray.get(object_ref)
assert value_before == value_after
for i in range(100):
value_before = "h" * i
object_ref = ray.put(value_before)
value_after = ray.get(object_ref)
assert value_before == value_after
for i in range(100):
value_before = [1] * i
object_ref = ray.put(value_before)
value_after = ray.get(object_ref)
assert value_before == value_after
@pytest.mark.skipif(client_test_enabled(), reason="internal _raylet")
def test_function_descriptor():
python_descriptor = ray._raylet.PythonFunctionDescriptor(
"module_name", "function_name", "class_name", "function_hash"
)
python_descriptor2 = pickle.loads(pickle.dumps(python_descriptor))
assert python_descriptor == python_descriptor2
assert hash(python_descriptor) == hash(python_descriptor2)
assert python_descriptor.function_id == python_descriptor2.function_id
java_descriptor = ray._raylet.JavaFunctionDescriptor(
"class_name", "function_name", "signature"
)
java_descriptor2 = pickle.loads(pickle.dumps(java_descriptor))
assert java_descriptor == java_descriptor2
assert python_descriptor != java_descriptor
assert python_descriptor != object()
d = {python_descriptor: 123}
assert d.get(python_descriptor2) == 123
def test_ray_options(shutdown_only):
ray.init(num_cpus=10, num_gpus=10, resources={"custom1": 2})
@ray.remote(num_cpus=2, num_gpus=3, memory=150 * 2**20, resources={"custom1": 1})
def foo(expected_resources):
# Possibly wait until the available resources have been updated
# (there might be a delay due to heartbeats)
retries = 10
keys = ["CPU", "GPU", "custom1"]
while retries >= 0:
resources = ray.available_resources()
do_return = True
for key in keys:
if resources[key] != expected_resources[key]:
print(key, resources[key], expected_resources[key])
do_return = False
break
if do_return:
return resources["memory"]
time.sleep(0.1)
retries -= 1
raise RuntimeError("Number of retries exceeded")
expected_resources_without_options = {"CPU": 8.0, "GPU": 7.0, "custom1": 1.0}
memory_available_without_options = ray.get(
foo.remote(expected_resources_without_options)
)
expected_resources_with_options = {"CPU": 7.0, "GPU": 6.0, "custom1": 1.5}
memory_available_with_options = ray.get(
foo.options(
num_cpus=3, num_gpus=4, memory=50 * 2**20, resources={"custom1": 0.5}
).remote(expected_resources_with_options)
)
assert memory_available_without_options < memory_available_with_options
@pytest.mark.skipif(client_test_enabled(), reason="internal api")
@pytest.mark.parametrize(
"ray_start_cluster_head",
[
{
"num_cpus": 0,
"object_store_memory": 75 * 1024 * 1024,
"_system_config": {"automatic_object_spilling_enabled": False},
}
],
indirect=True,
)
def test_fetch_local(ray_start_cluster_head):
cluster = ray_start_cluster_head
cluster.add_node(num_cpus=2, object_store_memory=75 * 1024 * 1024)
signal_actor = SignalActor.remote()
@ray.remote
def put():
ray.wait([signal_actor.wait.remote()])
return bytearray(40 * 1024 * 1024) # 40 MB data
local_ref = ray.put(bytearray(40 * 1024 * 1024))
remote_ref = put.remote()
# Data is not ready in any node
(ready_ref, remaining_ref) = ray.wait([remote_ref], timeout=2, fetch_local=False)
assert (0, 1) == (len(ready_ref), len(remaining_ref))
ray.wait([signal_actor.send.remote()])
# Data is ready in some node, but not local node.
(ready_ref, remaining_ref) = ray.wait([remote_ref], fetch_local=False)
assert (1, 0) == (len(ready_ref), len(remaining_ref))
(ready_ref, remaining_ref) = ray.wait([remote_ref], timeout=2, fetch_local=True)
assert (0, 1) == (len(ready_ref), len(remaining_ref))
del local_ref
(ready_ref, remaining_ref) = ray.wait([remote_ref], fetch_local=True)
assert (1, 0) == (len(ready_ref), len(remaining_ref))
def test_nested_functions(ray_start_regular_shared):
# Make sure that remote functions can use other values that are defined
# after the remote function but before the first function invocation.
@ray.remote
def f():
return g(), ray.get(h.remote())
def g():
return 1
@ray.remote
def h():
return 2
assert ray.get(f.remote()) == (1, 2)
def test_recursive_remote_call(ray_start_regular_shared):
# Test a remote function that recursively calls itself.
@ray.remote
def factorial(n):
if n == 0:
return 1
return n * ray.get(factorial.remote(n - 1))
assert ray.get(factorial.remote(0)) == 1
assert ray.get(factorial.remote(1)) == 1
assert ray.get(factorial.remote(2)) == 2
assert ray.get(factorial.remote(3)) == 6
assert ray.get(factorial.remote(4)) == 24
assert ray.get(factorial.remote(5)) == 120
def test_mutually_recursive_functions(ray_start_regular_shared):
# Test remote functions that recursively call each other.
@ray.remote
def factorial_even(n):
assert n % 2 == 0
if n == 0:
return 1
return n * ray.get(factorial_odd.remote(n - 1))
@ray.remote
def factorial_odd(n):
assert n % 2 == 1
return n * ray.get(factorial_even.remote(n - 1))
assert ray.get(factorial_even.remote(4)) == 24
assert ray.get(factorial_odd.remote(5)) == 120
def test_ray_recursive_objects(ray_start_regular_shared):
class ClassA:
pass
# Make a list that contains itself.
lst = []
lst.append(lst)
# Make an object that contains itself as a field.
a1 = ClassA()
a1.field = a1
# Make two objects that contain each other as fields.
a2 = ClassA()
a3 = ClassA()
a2.field = a3
a3.field = a2
# Make a dictionary that contains itself.
d1 = {}
d1["key"] = d1
# Create a list of recursive objects.
recursive_objects = [lst, a1, a2, a3, d1]
# Serialize the recursive objects.
for obj in recursive_objects:
ray.put(obj)
def test_passing_arguments_by_value_out_of_the_box(ray_start_regular_shared):
@ray.remote
def f(x):
return x
# Test passing lambdas.
def temp():
return 1
assert ray.get(f.remote(temp))() == 1
assert ray.get(f.remote(lambda x: x + 1))(3) == 4
# Test sets.
assert ray.get(f.remote(set())) == set()
s = {1, (1, 2, "hi")}
assert ray.get(f.remote(s)) == s
# Test types.
assert ray.get(f.remote(int)) is int
assert ray.get(f.remote(float)) is float
assert ray.get(f.remote(str)) is str
class Foo:
def __init__(self):
pass
# Make sure that we can put and get a custom type. Note that the result
# won't be "equal" to Foo.
ray.get(ray.put(Foo))
def test_putting_object_that_closes_over_object_ref(ray_start_regular_shared):
# This test is here to prevent a regression of
# https://github.com/ray-project/ray/issues/1317.
class Foo:
def __init__(self):
self.val = ray.put(0)
def method(self):
_ = f
f = Foo()
ray.put(f)
def test_keyword_args(ray_start_regular_shared):
@ray.remote
def keyword_fct1(a, b="hello"):
return "{} {}".format(a, b)
@ray.remote
def keyword_fct2(a="hello", b="world"):
return "{} {}".format(a, b)
@ray.remote
def keyword_fct3(a, b, c="hello", d="world"):
return "{} {} {} {}".format(a, b, c, d)
x = keyword_fct1.remote(1)
assert ray.get(x) == "1 hello"
x = keyword_fct1.remote(1, "hi")
assert ray.get(x) == "1 hi"
x = keyword_fct1.remote(1, b="world")
assert ray.get(x) == "1 world"
x = keyword_fct1.remote(a=1, b="world")
assert ray.get(x) == "1 world"
x = keyword_fct2.remote(a="w", b="hi")
assert ray.get(x) == "w hi"
x = keyword_fct2.remote(b="hi", a="w")
assert ray.get(x) == "w hi"
x = keyword_fct2.remote(a="w")
assert ray.get(x) == "w world"
x = keyword_fct2.remote(b="hi")
assert ray.get(x) == "hello hi"
x = keyword_fct2.remote("w")
assert ray.get(x) == "w world"
x = keyword_fct2.remote("w", "hi")
assert ray.get(x) == "w hi"
x = keyword_fct3.remote(0, 1, c="w", d="hi")
assert ray.get(x) == "0 1 w hi"
x = keyword_fct3.remote(0, b=1, c="w", d="hi")
assert ray.get(x) == "0 1 w hi"
x = keyword_fct3.remote(a=0, b=1, c="w", d="hi")
assert ray.get(x) == "0 1 w hi"
x = keyword_fct3.remote(0, 1, d="hi", c="w")
assert ray.get(x) == "0 1 w hi"
x = keyword_fct3.remote(0, 1, c="w")
assert ray.get(x) == "0 1 w world"
x = keyword_fct3.remote(0, 1, d="hi")
assert ray.get(x) == "0 1 hello hi"
x = keyword_fct3.remote(0, 1)
assert ray.get(x) == "0 1 hello world"
x = keyword_fct3.remote(a=0, b=1)
assert ray.get(x) == "0 1 hello world"
# Check that we cannot pass invalid keyword arguments to functions.
@ray.remote
def f1():
return
@ray.remote
def f2(x, y=0, z=0):
return
# Make sure we get an exception if too many arguments are passed in.
with pytest.raises(TypeError):
f1.remote(3)
with pytest.raises(TypeError):
f1.remote(x=3)
with pytest.raises(TypeError):
f2.remote(0, w=0)
with pytest.raises(TypeError):
f2.remote(3, x=3)
# Make sure we get an exception if too many arguments are passed in.
with pytest.raises(TypeError):
f2.remote(1, 2, 3, 4)
@ray.remote
def f3(x):
return x
assert ray.get(f3.remote(4)) == 4
def test_args_starkwargs(ray_start_regular_shared):
def starkwargs(a, b, **kwargs):
return a, b, kwargs
class TestActor:
def starkwargs(self, a, b, **kwargs):
return a, b, kwargs
def test_function(fn, remote_fn):
assert fn(1, 2, x=3) == ray.get(remote_fn.remote(1, 2, x=3))
with pytest.raises(TypeError):
remote_fn.remote(3)
remote_test_function = ray.remote(test_function)
remote_starkwargs = ray.remote(starkwargs)
test_function(starkwargs, remote_starkwargs)
ray.get(remote_test_function.remote(starkwargs, remote_starkwargs))
remote_actor_class = ray.remote(TestActor)
remote_actor = remote_actor_class.remote()
actor_method = remote_actor.starkwargs
local_actor = TestActor()
local_method = local_actor.starkwargs
test_function(local_method, actor_method)
ray.get(remote_test_function.remote(local_method, actor_method))
def test_args_named_and_star(ray_start_regular_shared):
def hello(a, x="hello", **kwargs):
return a, x, kwargs
class TestActor:
def hello(self, a, x="hello", **kwargs):
return a, x, kwargs
def test_function(fn, remote_fn):
assert fn(1, x=2, y=3) == ray.get(remote_fn.remote(1, x=2, y=3))
assert fn(1, 2, y=3) == ray.get(remote_fn.remote(1, 2, y=3))
assert fn(1, y=3) == ray.get(remote_fn.remote(1, y=3))
assert fn(1,) == ray.get(
remote_fn.remote(
1,
)
)
assert fn(1) == ray.get(remote_fn.remote(1))
with pytest.raises(TypeError):
remote_fn.remote(1, 2, x=3)
remote_test_function = ray.remote(test_function)
remote_hello = ray.remote(hello)
test_function(hello, remote_hello)
ray.get(remote_test_function.remote(hello, remote_hello))
remote_actor_class = ray.remote(TestActor)
remote_actor = remote_actor_class.remote()
actor_method = remote_actor.hello
local_actor = TestActor()
local_method = local_actor.hello
test_function(local_method, actor_method)
ray.get(remote_test_function.remote(local_method, actor_method))
def test_oversized_function(ray_start_regular_shared):
bar = bytearray(800 * 1024 * 125)
@ray.remote
class Actor:
def foo(self):
return len(bar)
@ray.remote
def f():
return len(bar)
with pytest.raises(ValueError, match="The remote function .*f is too large"):
f.remote()
with pytest.raises(ValueError, match="The actor Actor is too large"):
Actor.remote()
def test_args_stars_after(ray_start_regular_shared):
def star_args_after(a="hello", b="heo", *args, **kwargs):
return a, b, args, kwargs
class TestActor:
def star_args_after(self, a="hello", b="heo", *args, **kwargs):
return a, b, args, kwargs
def test_function(fn, remote_fn):
assert fn("hi", "hello", 2) == ray.get(remote_fn.remote("hi", "hello", 2))
assert fn("hi", "hello", 2, hi="hi") == ray.get(
remote_fn.remote("hi", "hello", 2, hi="hi")
)
assert fn(hi="hi") == ray.get(remote_fn.remote(hi="hi"))
remote_test_function = ray.remote(test_function)
remote_star_args_after = ray.remote(star_args_after)
test_function(star_args_after, remote_star_args_after)
ray.get(remote_test_function.remote(star_args_after, remote_star_args_after))
remote_actor_class = ray.remote(TestActor)
remote_actor = remote_actor_class.remote()
actor_method = remote_actor.star_args_after
local_actor = TestActor()
local_method = local_actor.star_args_after
test_function(local_method, actor_method)
ray.get(remote_test_function.remote(local_method, actor_method))
@pytest.mark.skipif(client_test_enabled(), reason="internal api")
def test_object_id_backward_compatibility(ray_start_regular_shared):
# We've renamed Python's `ObjectID` to `ObjectRef`, and added a type
# alias for backward compatibility.
# This test is to make sure legacy code can still use `ObjectID`.
# TODO(hchen): once we completely remove Python's `ObjectID`,
# this test can be removed as well.
# Check that these 2 types are the same.
assert ray.ObjectID == ray.ObjectRef
object_ref = ray.put(1)
# Check that users can use either type in `isinstance`
assert isinstance(object_ref, ray.ObjectID)
assert isinstance(object_ref, ray.ObjectRef)
def test_nonascii_in_function_body(ray_start_regular_shared):
@ray.remote
def return_a_greek_char():
return "φ"
assert ray.get(return_a_greek_char.remote()) == "φ"
def test_failed_task(ray_start_regular_shared, error_pubsub):
@ray.remote
def throw_exception_fct1():
raise Exception("Test function 1 intentionally failed.")
@ray.remote
def throw_exception_fct2():
raise Exception("Test function 2 intentionally failed.")
@ray.remote(num_returns=3)
def throw_exception_fct3(x):
raise Exception("Test function 3 intentionally failed.")
throw_exception_fct1.remote()
throw_exception_fct1.remote()
x = throw_exception_fct2.remote()
try:
ray.get(x)
except Exception as e:
assert "Test function 2 intentionally failed." in str(e)
else:
# ray.get should throw an exception.
assert False
x, y, z = throw_exception_fct3.remote(1.0)
for ref in [x, y, z]:
try:
ray.get(ref)
except Exception as e:
assert "Test function 3 intentionally failed." in str(e)
else:
# ray.get should throw an exception.
assert False
class CustomException(ValueError):
def __init__(self, msg):
super().__init__(msg)
self.field = 1
def f(self):
return 2
@ray.remote
def f():
raise CustomException("This function failed.")
try:
ray.get(f.remote())
except Exception as e:
assert "This function failed." in str(e)
assert isinstance(e, ValueError)
assert isinstance(e, CustomException)
assert isinstance(e, ray.exceptions.RayTaskError)
assert "RayTaskError(CustomException)" in repr(e)
assert e.field == 1
assert e.f() == 2
else:
# ray.get should throw an exception.
assert False
def test_base_exception_raised(ray_start_regular_shared):
@ray.remote
def f():
raise BaseException("rip")
return 1
with pytest.raises(BaseException):
ray.get(f.remote())
def test_import_ray_does_not_import_grpc():
# First unload grpc and ray
if "grpc" in sys.modules:
del sys.modules["grpc"]
if "ray" in sys.modules:
del sys.modules["ray"]
# Then import ray from scratch
import ray # noqa: F401
# Make sure grpc did not get imported by "import ray"
assert "grpc" not in sys.modules
# Load grpc back so other tests will not be affected
try:
import grpc # noqa: F401
except ImportError:
# It's ok if we don't have grpc installed.
pass
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))