409 lines
12 KiB
Python
409 lines
12 KiB
Python
# coding: utf-8
|
|
import os
|
|
import sys
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
import ray
|
|
import ray.cluster_utils
|
|
from ray._common.test_utils import wait_for_condition
|
|
from ray.dag import InputNode
|
|
from ray.exceptions import RayChannelError, RayTaskError
|
|
from ray.experimental.channel.conftest import (
|
|
Barrier,
|
|
start_nccl_mock,
|
|
)
|
|
from ray.tests.conftest import * # noqa
|
|
|
|
|
|
def error_logged(capsys, msg):
|
|
out, err = capsys.readouterr()
|
|
# Write captured back to stdout, stderr for easier test debugging.
|
|
sys.stdout.write(out)
|
|
sys.stderr.write(err)
|
|
return msg in err
|
|
|
|
|
|
@ray.remote(num_cpus=0, num_gpus=1)
|
|
class MockedWorker:
|
|
def __init__(self):
|
|
self.chan = None
|
|
|
|
def start_mock(self):
|
|
"""
|
|
Patch methods that require CUDA.
|
|
"""
|
|
start_nccl_mock()
|
|
|
|
def send(self, shape, dtype, value: int, send_as_dict=False):
|
|
if send_as_dict:
|
|
return self.send_dict([(value, value, shape, dtype)])
|
|
|
|
return torch.ones(shape, dtype=dtype) * value
|
|
|
|
def recv(self, tensor):
|
|
if isinstance(tensor, dict):
|
|
assert len(tensor) == 1
|
|
tensor = list(tensor.values())[0]
|
|
|
|
return (tensor[0].item(), tensor.shape, tensor.dtype)
|
|
|
|
def send_dict(self, entries):
|
|
results = {}
|
|
for key, value, shape, dtype in entries:
|
|
results[key] = torch.ones(shape, dtype=dtype) * value
|
|
return results
|
|
|
|
def recv_dict(self, tensor_dict):
|
|
results = []
|
|
for key in sorted(tensor_dict.keys()):
|
|
tensor = tensor_dict[key]
|
|
results.append((key, tensor[0].item(), tensor.shape, tensor.dtype))
|
|
return results
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ray_start_cluster",
|
|
[
|
|
{
|
|
"num_cpus": 2,
|
|
"num_gpus": 2,
|
|
"num_nodes": 1,
|
|
}
|
|
],
|
|
indirect=True,
|
|
)
|
|
def test_p2p(ray_start_cluster):
|
|
"""
|
|
Test simple sender -> receiver pattern. Check that receiver receives
|
|
correct results.
|
|
"""
|
|
# Barrier name should be barrier-{lower rank}-{higher rank}.
|
|
barrier = Barrier.options(name="barrier-0-1").remote() # noqa
|
|
|
|
sender = MockedWorker.remote()
|
|
receiver = MockedWorker.remote()
|
|
|
|
ray.get([sender.start_mock.remote(), receiver.start_mock.remote()])
|
|
|
|
shape = (10,)
|
|
dtype = torch.float16
|
|
|
|
# Test torch.Tensor sent between actors.
|
|
with InputNode() as inp:
|
|
dag = sender.send.bind(inp.shape, inp.dtype, inp[0], inp.send_as_dict)
|
|
dag = dag.with_tensor_transport(transport="nccl")
|
|
dag = receiver.recv.bind(dag)
|
|
|
|
compiled_dag = dag.experimental_compile()
|
|
for i in range(3):
|
|
ref = compiled_dag.execute(i, shape=shape, dtype=dtype, send_as_dict=False)
|
|
assert ray.get(ref) == (i, shape, dtype)
|
|
|
|
# Sending tensors of different shape also works.
|
|
for i in range(3):
|
|
ref = compiled_dag.execute(i, shape=(20,), dtype=dtype, send_as_dict=False)
|
|
assert ray.get(ref) == (i, (20,), dtype)
|
|
|
|
# Sending tensors inside a dictionary also works.
|
|
for i in range(3):
|
|
ref = compiled_dag.execute(i, shape=shape, dtype=dtype, send_as_dict=True)
|
|
assert ray.get(ref) == (i, shape, dtype)
|
|
|
|
compiled_dag.teardown()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ray_start_cluster",
|
|
[
|
|
{
|
|
"num_cpus": 2,
|
|
"num_gpus": 2,
|
|
"num_nodes": 1,
|
|
}
|
|
],
|
|
indirect=True,
|
|
)
|
|
@pytest.mark.parametrize("send_as_dict", [True, False])
|
|
def test_p2p_static_shape(ray_start_cluster, send_as_dict):
|
|
"""
|
|
Test simple send -> recv pattern with
|
|
_static_shape=True. If sender always sends tensors of
|
|
the same shape, then it works.
|
|
"""
|
|
# Barrier name should be barrier-{lower rank}-{higher rank}.
|
|
barrier = Barrier.options(name="barrier-0-1").remote() # noqa
|
|
|
|
sender = MockedWorker.remote()
|
|
receiver = MockedWorker.remote()
|
|
|
|
ray.get([sender.start_mock.remote(), receiver.start_mock.remote()])
|
|
|
|
shape = (10,)
|
|
dtype = torch.float16
|
|
|
|
# Test torch.Tensor sent between actors.
|
|
with InputNode() as inp:
|
|
dag = sender.send.bind(inp.shape, inp.dtype, inp[0], send_as_dict=send_as_dict)
|
|
dag = dag.with_tensor_transport(transport="nccl", _static_shape=True)
|
|
dag = receiver.recv.bind(dag)
|
|
|
|
compiled_dag = dag.experimental_compile()
|
|
for i in range(3):
|
|
ref = compiled_dag.execute(i, shape=shape, dtype=dtype)
|
|
assert ray.get(ref) == (i, shape, dtype)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ray_start_cluster",
|
|
[
|
|
{
|
|
"num_cpus": 2,
|
|
"num_gpus": 2,
|
|
"num_nodes": 1,
|
|
}
|
|
],
|
|
indirect=True,
|
|
)
|
|
@pytest.mark.parametrize("send_as_dict", [True, False])
|
|
def test_p2p_static_shape_error(capsys, ray_start_cluster, send_as_dict):
|
|
"""
|
|
Test that when static_shape=True, an error is thrown when a tensor with a
|
|
different shape or dtype is found.
|
|
"""
|
|
# Barrier name should be barrier-{lower rank}-{higher rank}.
|
|
barrier = Barrier.options(name="barrier-0-1").remote() # noqa
|
|
|
|
sender = MockedWorker.remote()
|
|
receiver = MockedWorker.remote()
|
|
|
|
ray.get([sender.start_mock.remote(), receiver.start_mock.remote()])
|
|
|
|
shape = (10,)
|
|
dtype = torch.float16
|
|
|
|
# Test torch.Tensor sent between actors.
|
|
with InputNode() as inp:
|
|
dag = sender.send.bind(inp.shape, inp.dtype, inp[0], send_as_dict=send_as_dict)
|
|
dag = dag.with_tensor_transport(transport="nccl", _static_shape=True)
|
|
dag = receiver.recv.bind(dag)
|
|
|
|
compiled_dag = dag.experimental_compile()
|
|
for i in range(3):
|
|
ref = compiled_dag.execute(i, shape=shape, dtype=dtype)
|
|
assert ray.get(ref) == (i, shape, dtype)
|
|
|
|
# Sending wrong shape errors.
|
|
ref = compiled_dag.execute(i, shape=(20,), dtype=dtype)
|
|
with pytest.raises(RayTaskError):
|
|
ray.get(ref)
|
|
|
|
# Sending correct shape still errors because the DAG has already been torn
|
|
# down after the previous error.
|
|
with pytest.raises(RayChannelError):
|
|
ref = compiled_dag.execute(i, shape=shape, dtype=dtype)
|
|
|
|
wait_for_condition(
|
|
lambda: error_logged(
|
|
capsys,
|
|
"ValueError: Expected torch.Tensors with shapes and dtypes: "
|
|
"[(shape=torch.Size([10]), dtype=torch.float16)], found: "
|
|
"[(shape=torch.Size([20]), dtype=torch.float16)]",
|
|
)
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ray_start_cluster",
|
|
[
|
|
{
|
|
"num_cpus": 2,
|
|
"num_gpus": 2,
|
|
"num_nodes": 1,
|
|
}
|
|
],
|
|
indirect=True,
|
|
)
|
|
def test_p2p_direct_return(ray_start_cluster):
|
|
"""
|
|
Test simple sender -> receiver pattern with _direct_return=True
|
|
"""
|
|
# Barrier name should be barrier-{lower rank}-{higher rank}.
|
|
barrier = Barrier.options(name="barrier-0-1").remote() # noqa
|
|
|
|
sender = MockedWorker.remote()
|
|
receiver = MockedWorker.remote()
|
|
|
|
ray.get([sender.start_mock.remote(), receiver.start_mock.remote()])
|
|
|
|
# Test torch.Tensor sent between actors.
|
|
with InputNode() as inp:
|
|
dag = sender.send.bind(inp.shape, inp.dtype, inp.value, inp.send_as_dict)
|
|
dag = dag.with_tensor_transport(
|
|
transport="nccl",
|
|
_direct_return=True,
|
|
)
|
|
dag = receiver.recv.bind(dag)
|
|
|
|
compiled_dag = dag.experimental_compile()
|
|
|
|
dtype = torch.float16
|
|
for i in range(3):
|
|
shape = (10 * (i + 1),)
|
|
ref = compiled_dag.execute(
|
|
shape=shape, dtype=dtype, value=i, send_as_dict=False
|
|
)
|
|
assert ray.get(ref) == (i, shape, dtype)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ray_start_cluster",
|
|
[
|
|
{
|
|
"num_cpus": 2,
|
|
"num_gpus": 2,
|
|
"num_nodes": 1,
|
|
}
|
|
],
|
|
indirect=True,
|
|
)
|
|
def test_p2p_direct_return_error(capsys, ray_start_cluster):
|
|
"""
|
|
Test simple sender -> receiver pattern with
|
|
_direct_return=True. Test that error is thrown when
|
|
actor task does not return a tensor directly.
|
|
"""
|
|
# Barrier name should be barrier-{lower rank}-{higher rank}.
|
|
barrier = Barrier.options(name="barrier-0-1").remote() # noqa
|
|
|
|
sender = MockedWorker.remote()
|
|
receiver = MockedWorker.remote()
|
|
|
|
ray.get([sender.start_mock.remote(), receiver.start_mock.remote()])
|
|
|
|
# Test torch.Tensor sent between actors.
|
|
with InputNode() as inp:
|
|
dag = sender.send.bind(inp.shape, inp.dtype, inp.value, inp.send_as_dict)
|
|
dag = dag.with_tensor_transport(
|
|
transport="nccl",
|
|
_direct_return=True,
|
|
)
|
|
dag = receiver.recv.bind(dag)
|
|
|
|
compiled_dag = dag.experimental_compile()
|
|
|
|
dtype = torch.float16
|
|
for i in range(3):
|
|
shape = (10 * (i + 1),)
|
|
ref = compiled_dag.execute(
|
|
shape=shape, dtype=dtype, value=i, send_as_dict=False
|
|
)
|
|
assert ray.get(ref) == (i, shape, dtype)
|
|
|
|
# Error is thrown if we do not send a tensor.
|
|
ref = compiled_dag.execute(shape=shape, dtype=dtype, value=1, send_as_dict=True)
|
|
with pytest.raises(RayTaskError):
|
|
ray.get(ref)
|
|
|
|
# Currently the receiver cannot catch the exception so the DAG cannot be
|
|
# used again.
|
|
with pytest.raises(RayChannelError):
|
|
ref = compiled_dag.execute(
|
|
shape=shape, dtype=dtype, value=1, send_as_dict=False
|
|
)
|
|
|
|
wait_for_condition(
|
|
lambda: error_logged(
|
|
capsys,
|
|
"Task annotated with _direct_return=True must "
|
|
"return a CUDA torch.Tensor",
|
|
)
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ray_start_cluster",
|
|
[
|
|
{
|
|
"num_cpus": 2,
|
|
"num_gpus": 2,
|
|
"num_nodes": 1,
|
|
}
|
|
],
|
|
indirect=True,
|
|
)
|
|
@pytest.mark.parametrize("check_static_shape", [True, False])
|
|
def test_p2p_static_shape_and_direct_return(
|
|
capsys, ray_start_cluster, check_static_shape
|
|
):
|
|
"""
|
|
Test simple sender -> receiver pattern with both _static_shape=True and
|
|
_direct_return=True. Check errors are thrown if tensors with wrong shape
|
|
are passed (check_static_shape=True) OR if non-tensor value is returned
|
|
(check_static_shape=False).
|
|
"""
|
|
# Barrier name should be barrier-{lower rank}-{higher rank}.
|
|
barrier = Barrier.options(name="barrier-0-1").remote() # noqa
|
|
|
|
sender = MockedWorker.remote()
|
|
receiver = MockedWorker.remote()
|
|
|
|
ray.get([sender.start_mock.remote(), receiver.start_mock.remote()])
|
|
|
|
# Test torch.Tensor sent between actors.
|
|
with InputNode() as inp:
|
|
dag = sender.send.bind(inp.shape, inp.dtype, inp.value, inp.send_as_dict)
|
|
dag = dag.with_tensor_transport(
|
|
transport="nccl",
|
|
_static_shape=True,
|
|
_direct_return=True,
|
|
)
|
|
dag = receiver.recv.bind(dag)
|
|
|
|
compiled_dag = dag.experimental_compile()
|
|
|
|
shape = (10,)
|
|
dtype = torch.float16
|
|
for i in range(3):
|
|
ref = compiled_dag.execute(
|
|
shape=shape, dtype=dtype, value=i, send_as_dict=False
|
|
)
|
|
assert ray.get(ref) == (i, shape, dtype)
|
|
|
|
if check_static_shape:
|
|
# Error is thrown if we send the wrong shape.
|
|
ref = compiled_dag.execute(
|
|
shape=(20,), dtype=dtype, value=1, send_as_dict=False
|
|
)
|
|
else:
|
|
# Error is thrown if we do not send a tensor.
|
|
ref = compiled_dag.execute(shape=shape, dtype=dtype, value=1, send_as_dict=True)
|
|
|
|
with pytest.raises(RayTaskError):
|
|
ray.get(ref)
|
|
|
|
# Currently the receiver cannot catch either kind of
|
|
# exception so the DAG cannot be used again.
|
|
with pytest.raises(RayChannelError):
|
|
ref = compiled_dag.execute(
|
|
shape=shape, dtype=dtype, value=1, send_as_dict=False
|
|
)
|
|
|
|
if check_static_shape:
|
|
msg = (
|
|
"ValueError: Expected torch.Tensors with shapes and dtypes: "
|
|
"[(shape=torch.Size([10]), dtype=torch.float16)], found: "
|
|
"[(shape=torch.Size([20]), dtype=torch.float16)]"
|
|
)
|
|
else:
|
|
msg = "Task annotated with _direct_return=True must return a CUDA torch.Tensor"
|
|
wait_for_condition(lambda: error_logged(capsys, msg))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if os.environ.get("PARALLEL_CI"):
|
|
sys.exit(pytest.main(["-n", "auto", "--boxed", "-vs", __file__]))
|
|
else:
|
|
sys.exit(pytest.main(["-sv", __file__]))
|