1205 lines
35 KiB
Python
1205 lines
35 KiB
Python
# coding: utf-8
|
|
import logging
|
|
import os
|
|
import pickle
|
|
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."
|
|
)
|
|
def test_worker_thread_count(monkeypatch, shutdown_only):
|
|
"""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!
|
|
"""
|
|
|
|
@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")
|
|
|
|
# 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 {21, 22, 23, 24}
|
|
|
|
|
|
# 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
|
|
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__]))
|