chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
:orphan:
.. _accelerator_types:
Accelerator types
=================
Ray supports the following accelerator types:
.. literalinclude:: ../../../python/ray/util/accelerators/accelerators.py
:language: python
+557
View File
@@ -0,0 +1,557 @@
.. _ray-remote-classes:
.. _actor-guide:
Actors
======
Actors extend the Ray API from functions (tasks) to classes.
An actor is essentially a stateful worker (or a service).
When you instantiate a new actor, Ray creates a new worker and schedules methods of the actor on
that specific worker. The methods can access and mutate the state of that worker.
.. tab-set::
.. tab-item:: Python
The ``ray.remote`` decorator indicates that instances of the ``Counter`` class are actors. Each actor runs in its own Python process.
.. testcode::
import ray
@ray.remote
class Counter:
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
return self.value
def get_counter(self):
return self.value
# Create an actor from this class.
counter = Counter.remote()
.. tab-item:: Java
``Ray.actor`` is used to create actors from regular Java classes.
.. code-block:: java
// A regular Java class.
public class Counter {
private int value = 0;
public int increment() {
this.value += 1;
return this.value;
}
}
// Create an actor from this class.
// `Ray.actor` takes a factory method that can produce
// a `Counter` object. Here, we pass `Counter`'s constructor
// as the argument.
ActorHandle<Counter> counter = Ray.actor(Counter::new).remote();
.. tab-item:: C++
``ray::Actor`` is used to create actors from regular C++ classes.
.. code-block:: c++
// A regular C++ class.
class Counter {
private:
int value = 0;
public:
int Increment() {
value += 1;
return value;
}
};
// Factory function of Counter class.
static Counter *CreateCounter() {
return new Counter();
};
RAY_REMOTE(&Counter::Increment, CreateCounter);
// Create an actor from this class.
// `ray::Actor` takes a factory method that can produce
// a `Counter` object. Here, we pass `Counter`'s factory function
// as the argument.
auto counter = ray::Actor(CreateCounter).Remote();
Use `ray list actors` from :ref:`State API <state-api-overview-ref>` to see actors states:
.. code-block:: bash
# This API is only available when you install Ray with `pip install "ray[default]"`.
ray list actors
.. code-block:: bash
======== List: 2023-05-25 10:10:50.095099 ========
Stats:
------------------------------
Total: 1
Table:
------------------------------
ACTOR_ID CLASS_NAME STATE JOB_ID NAME NODE_ID PID RAY_NAMESPACE
0 9e783840250840f87328c9f201000000 Counter ALIVE 01000000 13a475571662b784b4522847692893a823c78f1d3fd8fd32a2624923 38906 ef9de910-64fb-4575-8eb5-50573faa3ddf
Specifying required resources
-----------------------------
.. _actor-resource-guide:
Specify resource requirements in actors. See :ref:`resource-requirements` for more details.
.. tab-set::
.. tab-item:: Python
.. testcode::
# Specify required resources for an actor.
@ray.remote(num_cpus=2, num_gpus=0.5)
class Actor:
pass
.. tab-item:: Java
.. code-block:: java
// Specify required resources for an actor.
Ray.actor(Counter::new).setResource("CPU", 2.0).setResource("GPU", 0.5).remote();
.. tab-item:: C++
.. code-block:: c++
// Specify required resources for an actor.
ray::Actor(CreateCounter).SetResource("CPU", 2.0).SetResource("GPU", 0.5).Remote();
Calling the actor
-----------------
You can interact with the actor by calling its methods with the ``remote``
operator. You can then call ``get`` on the object ref to retrieve the actual
value.
.. tab-set::
.. tab-item:: Python
.. testcode::
# Call the actor.
obj_ref = counter.increment.remote()
print(ray.get(obj_ref))
.. testoutput::
1
.. tab-item:: Java
.. code-block:: java
// Call the actor.
ObjectRef<Integer> objectRef = counter.task(&Counter::increment).remote();
Assert.assertTrue(objectRef.get() == 1);
.. tab-item:: C++
.. code-block:: c++
// Call the actor.
auto object_ref = counter.Task(&Counter::increment).Remote();
assert(*object_ref.Get() == 1);
Methods called on different actors execute in parallel, and methods called on the same actor execute serially in the order you call them. Methods on the same actor share state with one another, as shown below.
.. tab-set::
.. tab-item:: Python
.. testcode::
# Create ten Counter actors.
counters = [Counter.remote() for _ in range(10)]
# Increment each Counter once and get the results. These tasks all happen in
# parallel.
results = ray.get([c.increment.remote() for c in counters])
print(results)
# Increment the first Counter five times. These tasks are executed serially
# and share state.
results = ray.get([counters[0].increment.remote() for _ in range(5)])
print(results)
.. testoutput::
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
[2, 3, 4, 5, 6]
.. tab-item:: Java
.. code-block:: java
// Create ten Counter actors.
List<ActorHandle<Counter>> counters = new ArrayList<>();
for (int i = 0; i < 10; i++) {
counters.add(Ray.actor(Counter::new).remote());
}
// Increment each Counter once and get the results. These tasks all happen in
// parallel.
List<ObjectRef<Integer>> objectRefs = new ArrayList<>();
for (ActorHandle<Counter> counterActor : counters) {
objectRefs.add(counterActor.task(Counter::increment).remote());
}
// prints [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
System.out.println(Ray.get(objectRefs));
// Increment the first Counter five times. These tasks are executed serially
// and share state.
objectRefs = new ArrayList<>();
for (int i = 0; i < 5; i++) {
objectRefs.add(counters.get(0).task(Counter::increment).remote());
}
// prints [2, 3, 4, 5, 6]
System.out.println(Ray.get(objectRefs));
.. tab-item:: C++
.. code-block:: c++
// Create ten Counter actors.
std::vector<ray::ActorHandle<Counter>> counters;
for (int i = 0; i < 10; i++) {
counters.emplace_back(ray::Actor(CreateCounter).Remote());
}
// Increment each Counter once and get the results. These tasks all happen in
// parallel.
std::vector<ray::ObjectRef<int>> object_refs;
for (ray::ActorHandle<Counter> counter_actor : counters) {
object_refs.emplace_back(counter_actor.Task(&Counter::Increment).Remote());
}
// prints 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
auto results = ray::Get(object_refs);
for (const auto &result : results) {
std::cout << *result;
}
// Increment the first Counter five times. These tasks are executed serially
// and share state.
object_refs.clear();
for (int i = 0; i < 5; i++) {
object_refs.emplace_back(counters[0].Task(&Counter::Increment).Remote());
}
// prints 2, 3, 4, 5, 6
results = ray::Get(object_refs);
for (const auto &result : results) {
std::cout << *result;
}
Passing around actor handles
----------------------------
You can pass actor handles into other tasks. You can also define remote functions or actor methods that use actor handles.
.. tab-set::
.. tab-item:: Python
.. testcode::
import time
@ray.remote
def f(counter):
for _ in range(10):
time.sleep(0.1)
counter.increment.remote()
.. tab-item:: Java
.. code-block:: java
public static class MyRayApp {
public static void foo(ActorHandle<Counter> counter) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
TimeUnit.MILLISECONDS.sleep(100);
counter.task(Counter::increment).remote();
}
}
}
.. tab-item:: C++
.. code-block:: c++
void Foo(ray::ActorHandle<Counter> counter) {
for (int i = 0; i < 1000; i++) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
counter.Task(&Counter::Increment).Remote();
}
}
If you instantiate an actor, you can pass the handle around to various tasks.
.. tab-set::
.. tab-item:: Python
.. testcode::
counter = Counter.remote()
# Start some tasks that use the actor.
[f.remote(counter) for _ in range(3)]
# Print the counter value.
for _ in range(10):
time.sleep(0.1)
print(ray.get(counter.get_counter.remote()))
.. testoutput::
:options: +MOCK
0
3
8
10
15
18
20
25
30
30
.. tab-item:: Java
.. code-block:: java
ActorHandle<Counter> counter = Ray.actor(Counter::new).remote();
// Start some tasks that use the actor.
for (int i = 0; i < 3; i++) {
Ray.task(MyRayApp::foo, counter).remote();
}
// Print the counter value.
for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.println(counter.task(Counter::getCounter).remote().get());
}
.. tab-item:: C++
.. code-block:: c++
auto counter = ray::Actor(CreateCounter).Remote();
// Start some tasks that use the actor.
for (int i = 0; i < 3; i++) {
ray::Task(Foo).Remote(counter);
}
// Print the counter value.
for (int i = 0; i < 10; i++) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << *counter.Task(&Counter::GetCounter).Remote().Get() << std::endl;
}
Type hints and static typing for actors
---------------------------------------
Ray supports Python type hints for both remote functions and actors, enabling better IDE support and static type checking. To get the best type inference and pass type checkers when working with actors, follow these patterns:
- **Prefer** ``ray.remote(MyClass)`` **over** ``@ray.remote`` **for actors**: Instead of decorating your class with ``@ray.remote``, use ``ActorClass = ray.remote(MyClass)``. This preserves the original class type and allows type checkers and IDEs to infer the correct types.
- **Use** ``@ray.method`` **for actor methods**: Decorate actor methods with ``@ray.method`` to enable type hints for remote method calls on actor handles.
- **Use the** ``ActorClass`` **and** ``ActorProxy`` **types**: When you instantiate an actor, annotate the handle as ``ActorProxy[MyClass]`` to get type hints for remote methods.
**Example:**
.. testcode::
import ray
from ray.actor import ActorClass, ActorProxy
class Counter:
def __init__(self):
self.value = 0
@ray.method
def increment(self) -> int:
self.value += 1
return self.value
CounterActor: ActorClass[Counter] = ray.remote(Counter)
counter: ActorProxy[Counter] = CounterActor.remote()
# Type checkers and IDEs will now provide type hints for remote methods
obj_ref: ray.ObjectRef[int] = counter.increment.remote()
print(ray.get(obj_ref))
For more details and advanced patterns, see :ref:`Type hints in Ray <core-type-hint>`.
Generators
----------
Ray is compatible with Python generator syntax. See :ref:`Ray Generators <generators>` for more details.
Cancelling actor tasks
----------------------
Cancel Actor Tasks by calling :func:`ray.cancel() <ray.cancel>` on the returned `ObjectRef`.
.. tab-set::
.. tab-item:: Python
.. literalinclude:: doc_code/actors.py
:language: python
:start-after: __cancel_start__
:end-before: __cancel_end__
In Ray, Task cancellation behavior is contingent on the Task's current state:
**Unscheduled tasks**:
If Ray hasn't scheduled an Actor Task yet, Ray attempts to cancel the scheduling.
When Ray successfully cancels at this stage, it invokes ``ray.get(actor_task_ref)``
which produces a :class:`TaskCancelledError <ray.exceptions.TaskCancelledError>`.
**Running actor tasks (regular actor, threaded actor)**:
For tasks classified as a single-threaded Actor or a multi-threaded Actor,
Ray sets a cancellation flag that can be checked via ``ray.get_runtime_context().is_canceled()``.
This allows for graceful cancellation by periodically checking the cancellation status within the task.
**Running async actor tasks**:
For Tasks classified as :ref:`async Actors <async-actors>`, Ray seeks to cancel the associated `asyncio.Task`.
This cancellation approach aligns with the standards presented in
`asyncio task cancellation <https://docs.python.org/3/library/asyncio-task.html#task-cancellation>`__.
Note that `asyncio.Task` won't be interrupted in the middle of execution if you don't `await` within the async function.
Note: ``ray.get_runtime_context().is_canceled()`` is not supported for async actors and will raise a ``RuntimeError``.
**Cancellation guarantee**:
Ray attempts to cancel Tasks on a *best-effort* basis, meaning cancellation isn't always guaranteed.
For example, if the cancellation request doesn't get through to the executor,
the Task might not be cancelled.
You can check if a Task was successfully cancelled using ``ray.get(actor_task_ref)``.
**Recursive cancellation**:
Ray tracks all child and Actor Tasks. When the ``recursive=True`` argument is given,
it cancels all child and Actor Tasks.
Detecting cancellation in running actor tasks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For non-async actor tasks, you can periodically check whether a cancellation has been requested
by calling ``ray.get_runtime_context().is_canceled()``. This allows tasks to detect cancellation
and perform cleanup operations before exiting gracefully.
.. tab-set::
.. tab-item:: Python
.. literalinclude:: doc_code/actors.py
:language: python
:start-after: __cancel_graceful_actor_start__
:end-before: __cancel_graceful_actor_end__
**Important notes:**
- For **non-async actor tasks**, direct interruption is not supported. You need to check ``is_canceled()`` periodically to detect cancellation requests.
- ``is_canceled()`` is **not supported** for async actor tasks and will raise a ``RuntimeError``.
Scheduling
----------
For each actor, Ray chooses a node to run it on,
and bases the scheduling decision on a few factors like
:ref:`the actor's resource requirements <ray-scheduling-resources>`
and :ref:`the specified scheduling strategy <ray-scheduling-strategies>`.
See :ref:`Ray scheduling <ray-scheduling>` for more details.
Fault Tolerance
---------------
By default, Ray actors won't be :ref:`restarted <fault-tolerance-actors>` and
actor tasks won't be retried when actors crash unexpectedly.
You can change this behavior by setting
``max_restarts`` and ``max_task_retries`` options
in :func:`ray.remote() <ray.remote>` and :meth:`.options() <ray.actor.ActorClass.options>`.
See :ref:`Ray fault tolerance <fault-tolerance>` for more details.
FAQ: Actors, Workers and Resources
----------------------------------
What's the difference between a worker and an actor?
Each "Ray worker" is a python process.
Ray treats a worker differently for tasks and actors. For tasks, Ray uses a "Ray worker" to execute multiple Ray tasks. For actors, Ray starts a "Ray worker" as a dedicated Ray actor.
* **Tasks**: When Ray starts on a machine, a number of Ray workers start automatically (1 per CPU by default). Ray uses them to execute tasks (like a process pool). If you execute 8 tasks with `num_cpus=2`, and total number of CPUs is 16 (`ray.cluster_resources()["CPU"] == 16`), you end up with 8 of your 16 workers idling.
* **Actor**: A Ray Actor is also a "Ray worker" but you instantiate it at runtime with `actor_cls.remote()`. All of its methods run on the same process, using the same resources Ray designates when you define the Actor. Note that unlike tasks, Ray doesn't reuse the Python processes that run Ray Actors. Ray terminates them when you delete the Actor.
To maximally utilize your resources, you want to maximize the time that
your workers work. You also want to allocate enough cluster resources
so Ray can run all of your needed actors and any other tasks you
define. This also implies that Ray schedules tasks more flexibly,
and that if you don't need the stateful part of an actor, it's better
to use tasks.
Task Events
-----------
By default, Ray traces the execution of actor tasks, reporting task status events and profiling events
that Ray Dashboard and :ref:`State API <state-api-overview-ref>` use.
You can disable task event reporting for the actor by setting the `enable_task_events` option to `False` in :func:`ray.remote() <ray.remote>` and :meth:`.options() <ray.actor.ActorClass.options>`. This setting reduces the overhead of task execution by reducing the amount of data Ray sends to the Ray Dashboard.
You can also disable task event reporting for some actor methods by setting the `enable_task_events` option to `False` in :func:`ray.remote() <ray.remote>` and :meth:`.options() <ray.remote_function.RemoteFunction.options>` on the actor method.
Method settings override the actor setting:
.. literalinclude:: doc_code/actors.py
:language: python
:start-after: __enable_task_events_start__
:end-before: __enable_task_events_end__
More about Ray Actors
---------------------
.. toctree::
:maxdepth: 1
actors/named-actors.rst
actors/terminating-actors.rst
actors/async_api.rst
actors/concurrency_group_api.rst
actors/actor-utils.rst
actors/out-of-band-communication.rst
actors/task-orders.rst
@@ -0,0 +1,34 @@
Utility Classes
===============
Actor Pool
~~~~~~~~~~
.. tab-set::
.. tab-item:: Python
The ``ray.util`` module contains a utility class, ``ActorPool``.
This class is similar to multiprocessing.Pool and lets you schedule Ray tasks over a fixed pool of actors.
.. literalinclude:: ../doc_code/actor-pool.py
See the :class:`package reference <ray.util.ActorPool>` for more information.
.. tab-item:: Java
Actor pool hasn't been implemented in Java yet.
.. tab-item:: C++
Actor pool hasn't been implemented in C++ yet.
Message passing using Ray Queue
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sometimes just using one signal to synchronize is not enough. If you need to send data among many tasks or
actors, you can use :class:`ray.util.queue.Queue <ray.util.queue.Queue>`.
.. literalinclude:: ../doc_code/actor-queue.py
Ray's Queue API has a similar API to Python's ``asyncio.Queue`` and ``queue.Queue``.
+335
View File
@@ -0,0 +1,335 @@
AsyncIO / Concurrency for Actors
================================
Within a single actor process, it is possible to execute concurrent threads.
Ray offers two types of concurrency within an actor:
* :ref:`async execution <async-actors>`
* :ref:`threading <threaded-actors>`
Keep in mind that the Python's `Global Interpreter Lock (GIL) <https://wiki.python.org/moin/GlobalInterpreterLock>`_ will only allow one thread of Python code running at once.
This means if you are just parallelizing Python code, you won't get true parallelism. If you call Numpy, Cython, Tensorflow, or PyTorch code, these libraries will release the GIL when calling into C/C++ functions.
**Neither the** :ref:`threaded-actors` nor :ref:`async-actors` **model will allow you to bypass the GIL.**
.. _async-actors:
AsyncIO for Actors
------------------
Since Python 3.5, it is possible to write concurrent code using the
``async/await`` `syntax <https://docs.python.org/3/library/asyncio.html>`__.
Ray natively integrates with asyncio. You can use Ray alongside popular
async frameworks like aiohttp, aioredis, etc.
.. testcode::
import ray
import asyncio
@ray.remote
class AsyncActor:
def __init__(self, expected_num_tasks: int):
self._event = asyncio.Event()
self._curr_num_tasks = 0
self._expected_num_tasks = expected_num_tasks
# Multiple invocations of this method can run concurrently on the same event loop.
async def run_concurrent(self):
self._curr_num_tasks += 1
if self._curr_num_tasks == self._expected_num_tasks:
print("All coroutines are executing concurrently, unblocking.")
self._event.set()
else:
print("Waiting for other coroutines to start.")
await self._event.wait()
print("All coroutines ran concurrently.")
actor = AsyncActor.remote(4)
refs = [actor.run_concurrent.remote() for _ in range(4)]
# Fetch results using regular `ray.get`.
ray.get(refs)
# Fetch results using `asyncio` APIs.
async def get_async():
return await asyncio.gather(*refs)
asyncio.run(get_async())
.. testoutput::
:options: +MOCK
(AsyncActor pid=9064) Waiting for other coroutines to start.
(AsyncActor pid=9064) Waiting for other coroutines to start.
(AsyncActor pid=9064) Waiting for other coroutines to start.
(AsyncActor pid=9064) All coroutines are executing concurrently, unblocking.
(AsyncActor pid=9064) All coroutines ran concurrently.
(AsyncActor pid=9064) All coroutines ran concurrently.
(AsyncActor pid=9064) All coroutines ran concurrently.
(AsyncActor pid=9064) All coroutines ran concurrently.
.. testcode::
:hide:
# NOTE: The outputs from the previous code block can show up in subsequent tests.
# To prevent flakiness, we wait for a grace period.
import time
print("Sleeping...")
time.sleep(1)
.. testoutput::
...
ObjectRefs as asyncio.Futures
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ObjectRefs can be translated to asyncio.Futures. This feature
make it possible to ``await`` on ray futures in existing concurrent
applications.
Instead of:
.. testcode::
import ray
@ray.remote
def some_task():
return 1
ray.get(some_task.remote())
ray.wait([some_task.remote()])
you can wait on the ref with Python 3.9 and Python 3.10:
.. testcode::
import ray
import asyncio
@ray.remote
def some_task():
return 1
async def await_obj_ref():
await some_task.remote()
await asyncio.wait([some_task.remote()])
asyncio.run(await_obj_ref())
or the Future object directly with Python 3.11+:
.. testcode::
import asyncio
async def convert_to_asyncio_future():
ref = some_task.remote()
fut: asyncio.Future = asyncio.wrap_future(ref.future())
print(await fut)
asyncio.run(convert_to_asyncio_future())
.. testoutput::
1
See the `asyncio doc <https://docs.python.org/3/library/asyncio-task.html>`__
for more `asyncio` patterns including timeouts and ``asyncio.gather``.
.. _async-ref-to-futures:
ObjectRefs as concurrent.futures.Futures
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ObjectRefs can also be wrapped into ``concurrent.futures.Future`` objects. This
is useful for interfacing with existing ``concurrent.futures`` APIs:
.. testcode::
import concurrent
refs = [some_task.remote() for _ in range(4)]
futs = [ref.future() for ref in refs]
for fut in concurrent.futures.as_completed(futs):
assert fut.done()
print(fut.result())
.. testoutput::
1
1
1
1
Defining an Async Actor
~~~~~~~~~~~~~~~~~~~~~~~
By using `async` method definitions, Ray will automatically detect whether an actor support `async` calls or not.
.. testcode::
import ray
import asyncio
@ray.remote
class AsyncActor:
def __init__(self, expected_num_tasks: int):
self._event = asyncio.Event()
self._curr_num_tasks = 0
self._expected_num_tasks = expected_num_tasks
async def run_task(self):
print("Started task")
self._curr_num_tasks += 1
if self._curr_num_tasks == self._expected_num_tasks:
self._event.set()
else:
# Yield the event loop for multiple coroutines to run concurrently.
await self._event.wait()
print("Finished task")
actor = AsyncActor.remote(5)
# All 5 tasks will start at once and run concurrently.
ray.get([actor.run_task.remote() for _ in range(5)])
.. testoutput::
:options: +MOCK
(AsyncActor pid=3456) Started task
(AsyncActor pid=3456) Started task
(AsyncActor pid=3456) Started task
(AsyncActor pid=3456) Started task
(AsyncActor pid=3456) Started task
(AsyncActor pid=3456) Finished task
(AsyncActor pid=3456) Finished task
(AsyncActor pid=3456) Finished task
(AsyncActor pid=3456) Finished task
(AsyncActor pid=3456) Finished task
Under the hood, Ray runs all of the methods inside a single python event loop.
Please note that running blocking ``ray.get`` or ``ray.wait`` inside async
actor method is not allowed, because ``ray.get`` will block the execution
of the event loop.
In async actors, only one task can be running at any point in time (though tasks can be multiplexed). There will be only one thread in AsyncActor! See :ref:`threaded-actors` if you want a threadpool.
Setting concurrency in Async Actors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can set the number of "concurrent" task running at once using the
``max_concurrency`` flag. By default, 1000 tasks can be running concurrently.
.. testcode::
import asyncio
import ray
@ray.remote
class AsyncActor:
def __init__(self, batch_size: int):
self._event = asyncio.Event()
self._curr_tasks = 0
self._batch_size = batch_size
async def run_task(self):
print("Started task")
self._curr_tasks += 1
if self._curr_tasks == self._batch_size:
self._event.set()
else:
await self._event.wait()
self._event.clear()
self._curr_tasks = 0
print("Finished task")
actor = AsyncActor.options(max_concurrency=2).remote(2)
# Only 2 tasks will run concurrently.
# Once 2 finish, the next 2 should run.
ray.get([actor.run_task.remote() for _ in range(8)])
.. testoutput::
:options: +MOCK
(AsyncActor pid=5859) Started task
(AsyncActor pid=5859) Started task
(AsyncActor pid=5859) Finished task
(AsyncActor pid=5859) Finished task
(AsyncActor pid=5859) Started task
(AsyncActor pid=5859) Started task
(AsyncActor pid=5859) Finished task
(AsyncActor pid=5859) Finished task
(AsyncActor pid=5859) Started task
(AsyncActor pid=5859) Started task
(AsyncActor pid=5859) Finished task
(AsyncActor pid=5859) Finished task
(AsyncActor pid=5859) Started task
(AsyncActor pid=5859) Started task
(AsyncActor pid=5859) Finished task
(AsyncActor pid=5859) Finished task
.. _threaded-actors:
Threaded Actors
---------------
Sometimes, asyncio is not an ideal solution for your actor. For example, you may
have one method that performs some computation heavy task while blocking the event loop, not giving up control via ``await``. This would hurt the performance of an Async Actor because Async Actors can only execute 1 task at a time and rely on ``await`` to context switch.
Instead, you can use the ``max_concurrency`` Actor options without any async methods, allowing you to achieve threaded concurrency (like a thread pool).
.. warning::
When there is at least one ``async def`` method in actor definition, Ray
will recognize the actor as AsyncActor instead of ThreadedActor.
.. testcode::
@ray.remote
class ThreadedActor:
def task_1(self): print("I'm running in a thread!")
def task_2(self): print("I'm running in another thread!")
a = ThreadedActor.options(max_concurrency=2).remote()
ray.get([a.task_1.remote(), a.task_2.remote()])
.. testoutput::
:options: +MOCK
(ThreadedActor pid=4822) I'm running in a thread!
(ThreadedActor pid=4822) I'm running in another thread!
Each invocation of the threaded actor will be running in a thread pool. The size of the threadpool is limited by the ``max_concurrency`` value.
AsyncIO for Remote Tasks
------------------------
We don't support asyncio for remote tasks. The following snippet will fail:
.. testcode::
:skipif: True
@ray.remote
async def f():
pass
Instead, you can wrap the ``async`` function with a wrapper to run the task synchronously:
.. testcode::
async def f():
pass
@ray.remote
def wrapper():
import asyncio
asyncio.run(f())
@@ -0,0 +1,199 @@
Limiting Concurrency Per-Method with Concurrency Groups
=======================================================
Besides setting the max concurrency overall for an actor, Ray allows methods to be separated into *concurrency groups*, each with its own thread(s). This allows you to limit the concurrency per-method, e.g., allow a health-check method to be given its own concurrency quota separate from request serving methods.
.. tip:: Concurrency groups work with both asyncio and threaded actors. The syntax is the same.
.. _defining-concurrency-groups:
Defining Concurrency Groups
---------------------------
This defines two concurrency groups, "io" with max concurrency = 2 and
"compute" with max concurrency = 4. The methods ``f1`` and ``f2`` are
placed in the "io" group, and the methods ``f3`` and ``f4`` are placed
into the "compute" group. Note that there is always a default
concurrency group for actors, which has a default concurrency of 1000 for
AsyncIO actors and 1 otherwise.
.. tab-set::
.. tab-item:: Python
You can define concurrency groups for actors using the ``concurrency_group`` decorator argument:
.. testcode::
import ray
@ray.remote(concurrency_groups={"io": 2, "compute": 4})
class AsyncIOActor:
def __init__(self):
pass
@ray.method(concurrency_group="io")
async def f1(self):
pass
@ray.method(concurrency_group="io")
async def f2(self):
pass
@ray.method(concurrency_group="compute")
async def f3(self):
pass
@ray.method(concurrency_group="compute")
async def f4(self):
pass
async def f5(self):
pass
a = AsyncIOActor.remote()
a.f1.remote() # executed in the "io" group.
a.f2.remote() # executed in the "io" group.
a.f3.remote() # executed in the "compute" group.
a.f4.remote() # executed in the "compute" group.
a.f5.remote() # executed in the default group.
.. tab-item:: Java
You can define concurrency groups for concurrent actors using the API ``setConcurrencyGroups()`` argument:
.. code-block:: java
class ConcurrentActor {
public long f1() {
return Thread.currentThread().getId();
}
public long f2() {
return Thread.currentThread().getId();
}
public long f3(int a, int b) {
return Thread.currentThread().getId();
}
public long f4() {
return Thread.currentThread().getId();
}
public long f5() {
return Thread.currentThread().getId();
}
}
ConcurrencyGroup group1 =
new ConcurrencyGroupBuilder<ConcurrentActor>()
.setName("io")
.setMaxConcurrency(1)
.addMethod(ConcurrentActor::f1)
.addMethod(ConcurrentActor::f2)
.build();
ConcurrencyGroup group2 =
new ConcurrencyGroupBuilder<ConcurrentActor>()
.setName("compute")
.setMaxConcurrency(1)
.addMethod(ConcurrentActor::f3)
.addMethod(ConcurrentActor::f4)
.build();
ActorHandle<ConcurrentActor> myActor = Ray.actor(ConcurrentActor::new)
.setConcurrencyGroups(group1, group2)
.remote();
myActor.task(ConcurrentActor::f1).remote(); // executed in the "io" group.
myActor.task(ConcurrentActor::f2).remote(); // executed in the "io" group.
myActor.task(ConcurrentActor::f3, 3, 5).remote(); // executed in the "compute" group.
myActor.task(ConcurrentActor::f4).remote(); // executed in the "compute" group.
myActor.task(ConcurrentActor::f5).remote(); // executed in the "default" group.
.. _default-concurrency-group:
Default Concurrency Group
-------------------------
By default, methods are placed in a default concurrency group which has a concurrency limit of 1000 for AsyncIO actors and 1 otherwise.
The concurrency of the default group can be changed by setting the ``max_concurrency`` actor option.
.. tab-set::
.. tab-item:: Python
The following actor has 2 concurrency groups: "io" and "default".
The max concurrency of "io" is 2, and the max concurrency of "default" is 10.
.. testcode::
@ray.remote(concurrency_groups={"io": 2})
class AsyncIOActor:
async def f1(self):
pass
actor = AsyncIOActor.options(max_concurrency=10).remote()
.. tab-item:: Java
The following concurrent actor has 2 concurrency groups: "io" and "default".
The max concurrency of "io" is 2, and the max concurrency of "default" is 10.
.. code-block:: java
class ConcurrentActor {
public long f1() {
return Thread.currentThread().getId();
}
}
ConcurrencyGroup group =
new ConcurrencyGroupBuilder<ConcurrentActor>()
.setName("io")
.setMaxConcurrency(2)
.addMethod(ConcurrentActor::f1)
.build();
ActorHandle<ConcurrentActor> myActor = Ray.actor(ConcurrentActor::new)
.setConcurrencyGroups(group)
.setMaxConcurrency(10)
.remote();
.. _setting-the-concurrency-group-at-runtime:
Setting the Concurrency Group at Runtime
----------------------------------------
You can also dispatch actor methods into a specific concurrency group at runtime.
The following snippet demonstrates setting the concurrency group of the
``f2`` method dynamically at runtime.
.. tab-set::
.. tab-item:: Python
You can use the ``.options`` method.
.. testcode::
# Executed in the "io" group (as defined in the actor class).
a.f2.options().remote()
# Executed in the "compute" group.
a.f2.options(concurrency_group="compute").remote()
.. tab-item:: Java
You can use ``setConcurrencyGroup`` method.
.. code-block:: java
// Executed in the "io" group (as defined in the actor creation).
myActor.task(ConcurrentActor::f2).remote();
// Executed in the "compute" group.
myActor.task(ConcurrentActor::f2).setConcurrencyGroup("compute").remote();
+218
View File
@@ -0,0 +1,218 @@
Named Actors
============
An actor can be given a unique name within their :ref:`namespace <namespaces-guide>`.
This allows you to retrieve the actor from any job in the Ray cluster.
This can be useful if you cannot directly
pass the actor handle to the task that needs it, or if you are trying to
access an actor launched by another driver.
Note that the actor will still be garbage-collected if no handles to it
exist. See :ref:`actor-lifetimes` for more details.
.. tab-set::
.. tab-item:: Python
.. testcode::
import ray
@ray.remote
class Counter:
pass
# Create an actor with a name
counter = Counter.options(name="some_name").remote()
# Retrieve the actor later somewhere
counter = ray.get_actor("some_name")
.. tab-item:: Java
.. code-block:: java
// Create an actor with a name.
ActorHandle<Counter> counter = Ray.actor(Counter::new).setName("some_name").remote();
...
// Retrieve the actor later somewhere
Optional<ActorHandle<Counter>> counter = Ray.getActor("some_name");
Assert.assertTrue(counter.isPresent());
.. tab-item:: C++
.. code-block:: c++
// Create an actor with a globally unique name
ActorHandle<Counter> counter = ray::Actor(CreateCounter).SetGlobalName("some_name").Remote();
...
// Retrieve the actor later somewhere
boost::optional<ray::ActorHandle<Counter>> counter = ray::GetGlobalActor("some_name");
We also support non-global named actors in C++, which means that the actor name is only valid within the job and the actor cannot be accessed from another job.
.. code-block:: c++
// Create an actor with a job-scope-unique name
ActorHandle<Counter> counter = ray::Actor(CreateCounter).SetName("some_name").Remote();
...
// Retrieve the actor later somewhere in the same job
boost::optional<ray::ActorHandle<Counter>> counter = ray::GetActor("some_name");
.. note::
Named actors are scoped by namespace. If no namespace is assigned, they will
be placed in an anonymous namespace by default.
.. tab-set::
.. tab-item:: Python
.. testcode::
:skipif: True
import ray
@ray.remote
class Actor:
pass
# driver_1.py
# Job 1 creates an actor, "orange" in the "colors" namespace.
ray.init(address="auto", namespace="colors")
Actor.options(name="orange", lifetime="detached").remote()
# driver_2.py
# Job 2 is now connecting to a different namespace.
ray.init(address="auto", namespace="fruit")
# This fails because "orange" was defined in the "colors" namespace.
ray.get_actor("orange")
# You can also specify the namespace explicitly.
ray.get_actor("orange", namespace="colors")
# driver_3.py
# Job 3 connects to the original "colors" namespace
ray.init(address="auto", namespace="colors")
# This returns the "orange" actor we created in the first job.
ray.get_actor("orange")
.. tab-item:: Java
.. code-block:: java
import ray
class Actor {
}
// Driver1.java
// Job 1 creates an actor, "orange" in the "colors" namespace.
System.setProperty("ray.job.namespace", "colors");
Ray.init();
Ray.actor(Actor::new).setName("orange").remote();
// Driver2.java
// Job 2 is now connecting to a different namespace.
System.setProperty("ray.job.namespace", "fruits");
Ray.init();
// This fails because "orange" was defined in the "colors" namespace.
Optional<ActorHandle<Actor>> actor = Ray.getActor("orange");
Assert.assertFalse(actor.isPresent()); // actor.isPresent() is false.
// Driver3.java
System.setProperty("ray.job.namespace", "colors");
Ray.init();
// This returns the "orange" actor we created in the first job.
Optional<ActorHandle<Actor>> actor = Ray.getActor("orange");
Assert.assertTrue(actor.isPresent()); // actor.isPresent() is true.
Get-Or-Create a Named Actor
---------------------------
A common use case is to create an actor only if it doesn't exist.
Ray provides a ``get_if_exists`` option for actor creation that does this out of the box.
This method is available after you set a name for the actor via ``.options()``.
If the actor already exists, a handle to the actor will be returned
and the arguments will be ignored. Otherwise, a new actor will be
created with the specified arguments.
.. tab-set::
.. tab-item:: Python
.. literalinclude:: ../doc_code/get_or_create.py
.. tab-item:: Java
.. code-block:: java
// This feature is not yet available in Java.
.. tab-item:: C++
.. code-block:: c++
// This feature is not yet available in C++.
.. _actor-lifetimes:
Actor Lifetimes
---------------
Separately, actor lifetimes can be decoupled from the job, allowing an actor to persist even after the driver process of the job exits. We call these actors *detached*.
.. tab-set::
.. tab-item:: Python
.. testcode::
counter = Counter.options(name="CounterActor", lifetime="detached").remote()
The ``CounterActor`` will be kept alive even after the driver running above script
exits. Therefore it is possible to run the following script in a different
driver:
.. testcode::
counter = ray.get_actor("CounterActor")
Note that an actor can be named but not detached. If we only specified the
name without specifying ``lifetime="detached"``, then the CounterActor can
only be retrieved as long as the original driver is still running.
.. tab-item:: Java
.. code-block:: java
System.setProperty("ray.job.namespace", "lifetime");
Ray.init();
ActorHandle<Counter> counter = Ray.actor(Counter::new).setName("some_name").setLifetime(ActorLifetime.DETACHED).remote();
The CounterActor will be kept alive even after the driver running above process
exits. Therefore it is possible to run the following code in a different
driver:
.. code-block:: java
System.setProperty("ray.job.namespace", "lifetime");
Ray.init();
Optional<ActorHandle<Counter>> counter = Ray.getActor("some_name");
Assert.assertTrue(counter.isPresent());
.. tab-item:: C++
Customizing lifetime of an actor hasn't been implemented in C++ yet.
Unlike normal actors, detached actors are not automatically garbage-collected by Ray.
Detached actors must be manually destroyed once you are sure that they are no
longer needed. To do this, use ``ray.kill`` to :ref:`manually terminate <ray-kill-actors>` the actor.
After this call, the actor's name may be reused.
@@ -0,0 +1,36 @@
Out-of-band Communication
=========================
Typically, Ray actor communication is done through actor method calls and data is shared through the distributed object store.
However, in some use cases out-of-band communication can be useful.
Wrapping Library Processes
--------------------------
Many libraries already have mature, high-performance internal communication stacks and
they leverage Ray as a language-integrated actor scheduler.
The actual communication between actors is mostly done out-of-band using existing communication stacks.
For example, Horovod-on-Ray uses NCCL or MPI-based collective communications, and RayDP uses Spark's internal RPC and object manager.
See `Ray Distributed Library Patterns <https://www.anyscale.com/blog/ray-distributed-library-patterns>`_ for more details.
Ray Collective
--------------
Ray's collective communication library (\ ``ray.util.collective``\ ) allows efficient out-of-band collective and point-to-point communication between distributed CPUs or GPUs.
See :ref:`Ray Collective <ray-collective>` for more details.
HTTP Server
-----------
You can start an HTTP server inside the actor and expose HTTP endpoints to clients
so users outside of the Ray cluster can communicate with the actor.
.. tab-set::
.. tab-item:: Python
.. literalinclude:: ../doc_code/actor-http-server.py
Similarly, you can expose other types of servers as well (e.g., gRPC servers).
Limitations
-----------
When using out-of-band communication with Ray actors, keep in mind that Ray does not manage the calls between actors. This means that functionality like distributed reference counting will not work with out-of-band communication, so you should take care not to pass object references in this way.
+149
View File
@@ -0,0 +1,149 @@
.. _actor-task-order:
Actor Task Execution Order
==========================
Synchronous, Single-Threaded Actor
----------------------------------
In Ray, an actor receives tasks from multiple submitters (including driver and workers).
For tasks received from the same submitter, a synchronous, single-threaded actor executes
them in the order they were submitted, unless you set ``allow_out_of_order_execution``,
or Ray retries tasks. In other words, a given task will not be executed until previously
submitted tasks from the same submitter have finished execution.
For actors where `max_task_retries` is set to a non-zero number, the task
execution order is not guaranteed when task retries occur.
.. tab-set::
.. tab-item:: Python
.. testcode::
import ray
@ray.remote
class Counter:
def __init__(self):
self.value = 0
def add(self, addition):
self.value += addition
return self.value
counter = Counter.remote()
# For tasks from the same submitter,
# they are executed according to submission order.
value0 = counter.add.remote(1)
value1 = counter.add.remote(2)
# Output: 1. The first submitted task is executed first.
print(ray.get(value0))
# Output: 3. The later submitted task is executed later.
print(ray.get(value1))
.. testoutput::
1
3
However, the actor does not guarantee the execution order of the tasks from different
submitters. For example, suppose an unfulfilled argument blocks a previously submitted
task. In this case, the actor can still execute tasks submitted by a different worker.
.. tab-set::
.. tab-item:: Python
.. testcode::
import time
import ray
@ray.remote
class Counter:
def __init__(self):
self.value = 0
def add(self, addition):
self.value += addition
return self.value
counter = Counter.remote()
# Submit task from a worker
@ray.remote
def submitter(value):
return ray.get(counter.add.remote(value))
# Simulate delayed result resolution.
@ray.remote
def delayed_resolution(value):
time.sleep(1)
return value
# Submit tasks from different workers, with
# the first submitted task waiting for
# dependency resolution.
value0 = submitter.remote(delayed_resolution.remote(1))
value1 = submitter.remote(2)
# Output: 3. The first submitted task is executed later.
print(ray.get(value0))
# Output: 2. The later submitted task is executed first.
print(ray.get(value1))
.. testoutput::
3
2
Asynchronous or Threaded Actor
------------------------------
:ref:`Asynchronous or threaded actors <async-actors>` do not guarantee the
task execution order. This means the system might execute a task
even though previously submitted tasks are pending execution.
.. tab-set::
.. tab-item:: Python
.. testcode::
import time
import ray
@ray.remote
class AsyncCounter:
def __init__(self):
self.value = 0
async def add(self, addition):
self.value += addition
return self.value
counter = AsyncCounter.remote()
# Simulate delayed result resolution.
@ray.remote
def delayed_resolution(value):
time.sleep(1)
return value
# Submit tasks from the driver, with
# the first submitted task waiting for
# dependency resolution.
value0 = counter.add.remote(delayed_resolution.remote(1))
value1 = counter.add.remote(2)
# Output: 3. The first submitted task is executed later.
print(ray.get(value0))
# Output: 2. The later submitted task is executed first.
print(ray.get(value1))
.. testoutput::
3
2
@@ -0,0 +1,250 @@
Terminating Actors
==================
Actor processes will be terminated automatically when all copies of the
actor handle have gone out of scope in Python, or if the original creator
process dies. When actors terminate gracefully, Ray calls the actor's
``__ray_shutdown__()`` method if defined, allowing for cleanup of resources
(see :ref:`actor-cleanup` for details).
Note that automatic termination of actors is not yet supported in Java or C++.
.. _ray-kill-actors:
Manual termination via an actor handle
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In most cases, Ray will automatically terminate actors that have gone out of
scope, but you may sometimes need to terminate an actor forcefully. This should
be reserved for cases where an actor is unexpectedly hanging or leaking
resources, and for :ref:`detached actors <actor-lifetimes>`, which must be
manually destroyed.
.. tab-set::
.. tab-item:: Python
.. testcode::
import ray
@ray.remote
class Actor:
pass
actor_handle = Actor.remote()
ray.kill(actor_handle)
# Force kill: the actor exits immediately without cleanup.
# This will NOT call __ray_shutdown__() or atexit handlers.
.. tab-item:: Java
.. code-block:: java
actorHandle.kill();
// This will not go through the normal Java System.exit teardown logic, so any
// shutdown hooks installed in the actor using ``Runtime.addShutdownHook(...)`` will
// not be called.
.. tab-item:: C++
.. code-block:: c++
actor_handle.Kill();
// This will not go through the normal C++ std::exit
// teardown logic, so any exit handlers installed in
// the actor using ``std::atexit`` will not be called.
This will cause the actor to immediately exit its process, causing any current,
pending, and future tasks to fail with a ``RayActorError``. If you would like
Ray to :ref:`automatically restart <fault-tolerance-actors>` the actor, make sure to set a nonzero
``max_restarts`` in the ``@ray.remote`` options for the actor, then pass the
flag ``no_restart=False`` to ``ray.kill``.
For :ref:`named and detached actors <actor-lifetimes>`, calling ``ray.kill`` on
an actor handle destroys the actor and allows the name to be reused.
Use `ray list actors --detail` from :ref:`State API <state-api-overview-ref>` to see the death cause of dead actors:
.. code-block:: bash
# This API is only available when you download Ray via `pip install "ray[default]"`
ray list actors --detail
.. code-block:: bash
---
- actor_id: e8702085880657b355bf7ef001000000
class_name: Actor
state: DEAD
job_id: '01000000'
name: ''
node_id: null
pid: 0
ray_namespace: dbab546b-7ce5-4cbb-96f1-d0f64588ae60
serialized_runtime_env: '{}'
required_resources: {}
death_cause:
actor_died_error_context: # <---- You could see the error message w.r.t why the actor exits.
error_message: The actor is dead because `ray.kill` killed it.
owner_id: 01000000ffffffffffffffffffffffffffffffffffffffffffffffff
owner_ip_address: 127.0.0.1
ray_namespace: dbab546b-7ce5-4cbb-96f1-d0f64588ae60
class_name: Actor
actor_id: e8702085880657b355bf7ef001000000
never_started: true
node_ip_address: ''
pid: 0
name: ''
is_detached: false
placement_group_id: null
repr_name: ''
Manual termination within the actor
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If necessary, you can manually terminate an actor from within one of the actor methods.
This will kill the actor process and release resources associated/assigned to the actor.
.. tab-set::
.. tab-item:: Python
.. testcode::
@ray.remote
class Actor:
def exit(self):
ray.actor.exit_actor()
actor = Actor.remote()
actor.exit.remote()
This approach should generally not be necessary as actors are automatically garbage
collected. The ``ObjectRef`` resulting from the task can be waited on to wait
for the actor to exit (calling ``ray.get()`` on it will raise a ``RayActorError``).
.. tab-item:: Java
.. code-block:: java
Ray.exitActor();
Garbage collection for actors hasn't been implemented yet, so this is currently the
only way to terminate an actor gracefully. The ``ObjectRef`` resulting from the task
can be waited on to wait for the actor to exit (calling ``ObjectRef::get`` on it will
throw a ``RayActorException``).
.. tab-item:: C++
.. code-block:: c++
ray::ExitActor();
Garbage collection for actors hasn't been implemented yet, so this is currently the
only way to terminate an actor gracefully. The ``ObjectRef`` resulting from the task
can be waited on to wait for the actor to exit (calling ``ObjectRef::Get`` on it will
throw a ``RayActorException``).
Note that this method of termination waits until any previously submitted
tasks finish executing and then exits the process gracefully with sys.exit.
You could see the actor is dead as a result of the user's `exit_actor()` call:
.. code-block:: bash
# This API is only available when you download Ray via `pip install "ray[default]"`
ray list actors --detail
.. code-block:: bash
---
- actor_id: 070eb5f0c9194b851bb1cf1602000000
class_name: Actor
state: DEAD
job_id: '02000000'
name: ''
node_id: 47ccba54e3ea71bac244c015d680e202f187fbbd2f60066174a11ced
pid: 47978
ray_namespace: 18898403-dda0-485a-9c11-e9f94dffcbed
serialized_runtime_env: '{}'
required_resources: {}
death_cause:
actor_died_error_context:
error_message: 'The actor is dead because its worker process has died.
Worker exit type: INTENDED_USER_EXIT Worker exit detail: Worker exits
by a user request. exit_actor() is called.'
owner_id: 02000000ffffffffffffffffffffffffffffffffffffffffffffffff
owner_ip_address: 127.0.0.1
node_ip_address: 127.0.0.1
pid: 47978
ray_namespace: 18898403-dda0-485a-9c11-e9f94dffcbed
class_name: Actor
actor_id: 070eb5f0c9194b851bb1cf1602000000
name: ''
never_started: false
is_detached: false
placement_group_id: null
repr_name: ''
.. _actor-cleanup:
Actor cleanup with `__ray_shutdown__`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When an actor terminates gracefully, Ray calls the ``__ray_shutdown__()`` method
if it exists, allowing cleanup of resources like database connections or file handles.
.. tab-set::
.. tab-item:: Python
.. testcode::
import ray
import tempfile
import os
@ray.remote
class FileProcessorActor:
def __init__(self):
self.temp_file = tempfile.NamedTemporaryFile(delete=False)
self.temp_file.write(b"processing data")
self.temp_file.flush()
def __ray_shutdown__(self):
# Clean up temporary file
if hasattr(self, 'temp_file'):
self.temp_file.close()
os.unlink(self.temp_file.name)
def process(self):
return "done"
actor = FileProcessorActor.remote()
ray.get(actor.process.remote())
del actor # __ray_shutdown__() is called automatically
When ``__ray_shutdown__()`` is called:
- **Automatic termination**: When all actor handles go out of scope (``del actor`` or natural scope exit)
- **Manual graceful termination**: When you call ``actor.__ray_terminate__.remote()``
When ``__ray_shutdown__()`` is **NOT** called:
- **Force kill**: When you use ``ray.kill(actor)`` - the actor is killed immediately without cleanup.
- **Unexpected termination**: When the actor process crashes or exits unexpectedly (such as a segfault or being killed by the OOM killer).
**Important notes:**
- ``__ray_shutdown__()`` runs after all actor tasks complete.
- By default, Ray waits 30 seconds for the graceful shutdown procedure (including ``__ray_shutdown__()``) to complete. If the actor doesn't exit within this timeout, it's force killed. Configure this with ``ray.init(_system_config={"actor_graceful_shutdown_timeout_ms": 60000})``.
- Exceptions in ``__ray_shutdown__()`` are caught and logged but don't prevent actor termination.
- ``__ray_shutdown__()`` must be a synchronous method, including for async actors.
+20
View File
@@ -0,0 +1,20 @@
Advanced topics
===============
This section covers extended topics on how to use Ray.
.. toctree::
:maxdepth: -1
tips-for-first-time
type-hint
starting-ray
ray-generator
namespaces
cross-language
using-ray-with-jupyter
ray-dag
miscellaneous
runtime_env_auth
user-spawn-processes
head-node-memory-management
+53
View File
@@ -0,0 +1,53 @@
Ray Core CLI
============
.. _ray-cli:
Debugging applications
----------------------
This section contains commands for inspecting and debugging the current cluster.
.. _ray-stack-doc:
.. click:: ray.scripts.scripts:stack
:prog: ray stack
:show-nested:
.. _ray-memory-doc:
.. click:: ray.scripts.scripts:memory
:prog: ray memory
:show-nested:
.. _ray-timeline-doc:
.. click:: ray.scripts.scripts:timeline
:prog: ray timeline
:show-nested:
.. _ray-status-doc:
.. click:: ray.scripts.scripts:status
:prog: ray status
:show-nested:
.. click:: ray.scripts.scripts:debug
:prog: ray debug
:show-nested:
Usage Stats
-----------
This section contains commands to enable/disable :ref:`Ray usage stats <ref-usage-stats>`.
.. _ray-disable-usage-stats-doc:
.. click:: ray.scripts.scripts:disable_usage_stats
:prog: ray disable-usage-stats
:show-nested:
.. _ray-enable-usage-stats-doc:
.. click:: ray.scripts.scripts:enable_usage_stats
:prog: ray enable-usage-stats
:show-nested:
+75
View File
@@ -0,0 +1,75 @@
Core API
========
.. autosummary::
:nosignatures:
:toctree: doc/
ray.init
ray.shutdown
ray.is_initialized
ray.job_config.JobConfig
ray.LoggingConfig
Tasks
-----
.. autosummary::
:nosignatures:
:toctree: doc/
ray.remote
ray.remote_function.RemoteFunction.options
ray.cancel
Actors
------
.. autosummary::
:nosignatures:
:toctree: doc/
ray.remote
ray.actor.ActorClass
ray.actor.ActorClass.options
ray.actor.ActorMethod
ray.actor.ActorHandle
ray.actor.ActorClassInheritanceException
ray.actor.exit_actor
ray.method
ray.get_actor
ray.kill
Objects
-------
.. autosummary::
:nosignatures:
:toctree: doc/
ray.get
ray.wait
ray.put
ray.util.as_completed
ray.util.map_unordered
.. _runtime-context-apis:
Runtime Context
---------------
.. autosummary::
:nosignatures:
:toctree: doc/
ray.runtime_context.get_runtime_context
ray.runtime_context.RuntimeContext
ray.get_gpu_ids
Cross Language
--------------
.. autosummary::
:nosignatures:
:toctree: doc/
ray.cross_language.java_function
ray.cross_language.java_actor_class
@@ -0,0 +1,43 @@
Ray Direct Transport (RDT) API
==============================
Usage with Core APIs
--------------------
Enable RDT for actor tasks with the :func:`@ray.method <ray.method>` decorator, or pass `_tensor_transport` to :func:`ray.put`. You can then pass the resulting `ray.ObjectRef` to other actor tasks, or use :func:`ray.get` to retrieve the result. See :ref:`Ray Direct Transport (RDT) <direct-transport>` for more details on usage.
.. autosummary::
:nosignatures:
:toctree: doc/
ray.method
ray.put
ray.get
Collective tensor transports
----------------------------
Collective tensor transports require a collective group to be created before RDT objects can be used. Use these methods to create and manage collective groups for the `gloo` and `nccl` tensor transports.
.. autosummary::
:nosignatures:
:toctree: doc/
ray.experimental.collective.create_collective_group
ray.experimental.collective.get_collective_groups
ray.experimental.collective.destroy_collective_group
Advanced APIs
-------------
.. autosummary::
:nosignatures:
:toctree: doc/
ray.experimental.register_nixl_memory
ray.experimental.deregister_nixl_memory
ray.experimental.register_nixl_memory_pool
ray.experimental.set_target_for_ref
ray.experimental.wait_tensor_freed
ray.experimental.register_tensor_transport
ray.experimental.TensorTransportManager
+42
View File
@@ -0,0 +1,42 @@
.. _ray-core-exceptions:
Exceptions
==========
.. autosummary::
:nosignatures:
:toctree: doc/
ray.exceptions.RayError
ray.exceptions.RayTaskError
ray.exceptions.RayActorError
ray.exceptions.TaskCancelledError
ray.exceptions.TaskUnschedulableError
ray.exceptions.ActorDiedError
ray.exceptions.ActorUnschedulableError
ray.exceptions.ActorUnavailableError
ray.exceptions.AsyncioActorExit
ray.exceptions.LocalRayletDiedError
ray.exceptions.WorkerCrashedError
ray.exceptions.TaskPlacementGroupRemoved
ray.exceptions.ActorPlacementGroupRemoved
ray.exceptions.ObjectStoreFullError
ray.exceptions.OutOfDiskError
ray.exceptions.OutOfMemoryError
ray.exceptions.ObjectLostError
ray.exceptions.ObjectFetchTimedOutError
ray.exceptions.GetTimeoutError
ray.exceptions.OwnerDiedError
ray.exceptions.PendingCallsLimitExceeded
ray.exceptions.PlasmaObjectNotAvailable
ray.exceptions.ObjectReconstructionFailedError
ray.exceptions.RayChannelError
ray.exceptions.RayChannelTimeoutError
ray.exceptions.RayCgraphCapacityExceeded
ray.exceptions.RayDirectTransportError
ray.exceptions.RuntimeEnvSetupError
ray.exceptions.CrossLanguageError
ray.exceptions.RaySystemError
ray.exceptions.NodeDiedError
ray.exceptions.UnserializableException
ray.exceptions.AuthenticationError
+15
View File
@@ -0,0 +1,15 @@
Ray Core API
============
.. toctree::
:maxdepth: 2
core.rst
scheduling.rst
runtime-env.rst
utility.rst
exceptions.rst
cli.rst
../../ray-observability/reference/cli.rst
../../ray-observability/reference/api.rst
direct-transport.rst
+9
View File
@@ -0,0 +1,9 @@
Runtime Env API
===============
.. autosummary::
:nosignatures:
:toctree: doc/
ray.runtime_env.RuntimeEnvConfig
ray.runtime_env.RuntimeEnv
+28
View File
@@ -0,0 +1,28 @@
Scheduling API
==============
Scheduling Strategy
-------------------
.. autosummary::
:nosignatures:
:toctree: doc/
ray.util.scheduling_strategies.PlacementGroupSchedulingStrategy
ray.util.scheduling_strategies.NodeAffinitySchedulingStrategy
.. _ray-placement-group-ref:
Placement Group
---------------
.. autosummary::
:nosignatures:
:toctree: doc/
ray.util.placement_group
ray.util.placement_group.get_placement_group
ray.util.placement_group.PlacementGroup
ray.util.placement_group_table
ray.util.remove_placement_group
ray.util.get_current_placement_group
+62
View File
@@ -0,0 +1,62 @@
Utility
=======
.. autosummary::
:nosignatures:
:toctree: doc/
ray.util.ActorPool
ray.util.queue.Queue
ray.util.list_named_actors
ray.util.serialization.register_serializer
ray.util.serialization.deregister_serializer
ray.util.tpu.get_current_pod_worker_count
ray.util.tpu.get_current_pod_name
ray.util.tpu.get_num_tpu_chips_on_node
ray.util.tpu.get_tpu_coordinator_env_vars
ray.util.tpu.get_tpu_slice_name_from_node
ray.util.tpu.get_tpu_nodes_for_slice
ray.util.tpu.get_num_ready_tpu_slices
ray.util.tpu.get_tpu_num_slices_for_workers
ray.util.tpu.get_tpu_version_from_type
ray.util.tpu.get_tpu_worker_resources
ray.util.tpu.init_jax_profiler
ray.util.tpu.SlicePlacementGroup
ray.util.tpu.slice_placement_group
ray.nodes
ray.cluster_resources
ray.available_resources
.. Other docs have references to these
ray.util.queue.Empty
ray.util.queue.Full
.. _custom-metric-api-ref:
Custom Metrics
--------------
.. autosummary::
:nosignatures:
:toctree: doc/
ray.util.metrics.Counter
ray.util.metrics.Gauge
ray.util.metrics.Histogram
.. _package-ref-debugging-apis:
Debugging
---------
.. autosummary::
:nosignatures:
:toctree: doc/
ray.util.rpdb.set_trace
ray.util.inspect_serializability
ray.timeline
@@ -0,0 +1,43 @@
Compiled Graph API
==================
Input and Output Nodes
----------------------
.. autosummary::
:nosignatures:
:toctree: doc/
ray.dag.input_node.InputNode
ray.dag.output_node.MultiOutputNode
DAG Construction
----------------
.. autosummary::
:nosignatures:
:toctree: doc/
ray.actor.ActorMethod.bind
ray.dag.DAGNode.with_tensor_transport
ray.experimental.compiled_dag_ref.CompiledDAGRef
Compiled Graph Operations
-------------------------
.. autosummary::
:nosignatures:
:toctree: doc/
ray.dag.DAGNode.experimental_compile
ray.dag.compiled_dag_node.CompiledDAG.execute
ray.dag.compiled_dag_node.CompiledDAG.visualize
Configurations
--------------
.. autosummary::
:nosignatures:
:toctree: doc/
ray.dag.context.DAGContext
@@ -0,0 +1,25 @@
.. _compiled-graph-overlap:
Experimental: Overlapping communication and computation
=======================================================
Compiled Graph currently provides experimental support for GPU communication and computation overlap. When you turn this feature on, it automatically overlaps the GPU communication with computation operations, thereby hiding the communication overhead and improving performance.
To enable this feature, specify ``_overlap_gpu_communication=True`` when calling :func:`dag.experimental_compile() <ray.dag.DAGNode.experimental_compile>`.
The following code has GPU communication and computation operations that benefit
from overlapping.
.. literalinclude:: ../doc_code/cgraph_overlap.py
:language: python
:start-after: __cgraph_overlap_start__
:end-before: __cgraph_overlap_end__
The output of the preceding code includes the following two lines:
.. testoutput::
overlap_gpu_communication=False, duration=1.0670117866247892
overlap_gpu_communication=True, duration=0.9211348341777921
The actual performance numbers may vary on different hardware, but enabling ``_overlap_gpu_communication`` improves latency by about 14% for this example.
@@ -0,0 +1,87 @@
Profiling
=========
Ray Compiled Graph provides both PyTorch-based and Nsight-based profiling functionalities to better understand the performance
of individual tasks, system overhead, and performance bottlenecks. You can pick your favorite profiler based on your preference.
PyTorch profiler
----------------
To run PyTorch Profiling on Compiled Graph, simply set the environment variable ``RAY_CGRAPH_ENABLE_TORCH_PROFILING=1``
when running the script. For example, for a Compiled Graph script in ``example.py``, run the following command:
.. code-block:: bash
RAY_CGRAPH_ENABLE_TORCH_PROFILING=1 python3 example.py
After execution, Compiled Graph generates the profiling results in the `compiled_graph_torch_profiles` directory
under the current working directory. Compiled Graph generates one trace file per actor.
You can visualize traces by using https://ui.perfetto.dev/.
Nsight system profiler
----------------------
Compiled Graph builds on top of Ray's profiling capabilities, and leverages Nsight
system profiling.
To run Nsight Profiling on Compiled Graph, specify the runtime_env for the involved actors
as described in :ref:`Run Nsight on Ray <run-nsight-on-ray>`. For example,
.. literalinclude:: ../doc_code/cgraph_profiling.py
:language: python
:start-after: __profiling_setup_start__
:end-before: __profiling_setup_end__
Then, create a Compiled Graph as usual.
.. literalinclude:: ../doc_code/cgraph_profiling.py
:language: python
:start-after: __profiling_execution_start__
:end-before: __profiling_execution_end__
Finally, run the script as usual.
.. code-block:: bash
python3 example.py
After execution, Compiled Graph generates the profiling results under the `/tmp/ray/session_*/logs/{profiler_name}`
directory.
For fine-grained performance analysis of method calls and system overhead, set the environment variable
``RAY_CGRAPH_ENABLE_NVTX_PROFILING=1`` when running the script:
.. code-block:: bash
RAY_CGRAPH_ENABLE_NVTX_PROFILING=1 python3 example.py
This command leverages the `NVTX library <https://nvtx.readthedocs.io/en/latest/index.html#>`_ under the hood to automatically
annotate all methods called in the execution loops of compiled graph.
To visualize the profiling results, follow the same instructions as described in
:ref:`Nsight Profiling Result <profiling-result>`.
Visualization
-------------
To visualize the graph structure, call the :func:`visualize <ray.dag.compiled_dag_node.CompiledDAG.visualize>` method after calling :func:`experimental_compile <ray.dag.DAGNode.experimental_compile>`
on the graph.
.. literalinclude:: ../doc_code/cgraph_visualize.py
:language: python
:start-after: __cgraph_visualize_start__
:end-before: __cgraph_visualize_end__
By default, Ray generates a PNG image named ``compiled_graph.png`` and saves it in the current working directory.
Note that this requires ``graphviz``.
The following image shows the visualization for the preceding code.
Tasks that belong to the same actor are the same color.
.. image:: ../../images/compiled_graph_viz.png
:alt: Visualization of Graph Structure
:align: center
@@ -0,0 +1,260 @@
Quickstart
==========
Hello World
-----------
This "hello world" example uses Ray Compiled Graph. First, install Ray.
.. code-block:: bash
pip install "ray[cgraph]"
# For a ray version before 2.41, use the following instead:
# pip install "ray[adag]"
First, define a simple actor that echoes its argument.
.. literalinclude:: ../doc_code/cgraph_quickstart.py
:language: python
:start-after: __simple_actor_start__
:end-before: __simple_actor_end__
Next instantiate the actor and use the classic Ray Core APIs ``remote`` and ``ray.get`` to execute tasks on the actor.
.. literalinclude:: ../doc_code/cgraph_quickstart.py
:language: python
:start-after: __ray_core_usage_start__
:end-before: __ray_core_usage_end__
.. code-block::
Execution takes 969.0364822745323 us
Now, create an equivalent program using Ray Compiled Graph.
First, define a graph to execute using classic Ray Core, without any compilation.
Later, compile this graph, to apply optimizations and prevent further modifications to the graph.
First, create a :ref:`Ray DAG <ray-dag-guide>` (directed acyclic graph), which is a lazily executed graph of Ray tasks.
Note 3 key differences with the classic Ray Core APIs:
1. Use the :class:`ray.dag.InputNode <ray.dag.input_node.InputNode>` context manager to indicate which inputs to the DAG should be provided at run time.
2. Use :func:`bind() <ray.actor.ActorMethod.bind>` instead of :func:`remote() <ray.remote>` to indicate lazily executed Ray tasks.
3. Use :func:`execute() <ray.dag.compiled_dag_node.CompiledDAG.execute>` to execute the DAG.
Here, define a graph and execute it.
Note that there is **no** compilation happening here. This uses the same execution backend as the preceding example:
.. literalinclude:: ../doc_code/cgraph_quickstart.py
:language: python
:start-after: __dag_usage_start__
:end-before: __dag_usage_end__
Next, compile the ``dag`` using the :func:`experimental_compile <ray.dag.DAGNode.experimental_compile>` API.
The graph uses the same APIs for execution:
.. literalinclude:: ../doc_code/cgraph_quickstart.py
:language: python
:start-after: __cgraph_usage_start__
:end-before: __cgraph_usage_end__
.. code-block::
Execution takes 86.72196418046951 us
The performance of the same task graph improved by 10X. This is because the function ``echo`` is cheap and thus highly affected by
the system overhead. Due to various bookkeeping and distributed protocols, the classic Ray Core APIs usually have 1 ms+ system overhead.
Because the system knows the task graph ahead of time, Ray Compiled Graphs can pre-allocate all necessary
resources ahead of time and greatly reduce the system overhead.
For example, if the actor ``a`` is on the same node as the driver, Ray Compiled Graphs uses shared memory instead of RPC to transfer data directly between the driver and the actor.
Currently, the DAG tasks run on a **background thread** of the involved actors.
An actor can only participate in one DAG at a time.
Normal tasks can still execute on the actors while the actors participate in a Compiled Graph, but these tasks execute on the main thread.
Once you're done, you can tear down the Compiled Graph by deleting it or explicitly calling ``dag.teardown()``.
This allows reuse of the actors in a new Compiled Graph.
.. literalinclude:: ../doc_code/cgraph_quickstart.py
:language: python
:start-after: __teardown_start__
:end-before: __teardown_end__
Specifying data dependencies
----------------------------
When creating the DAG, a ``ray.dag.DAGNode`` can be passed as an argument to other ``.bind`` calls to specify data dependencies.
For example, the following uses the preceding example to create a DAG that passes the same message from one actor to another:
.. literalinclude:: ../doc_code/cgraph_quickstart.py
:language: python
:start-after: __cgraph_bind_start__
:end-before: __cgraph_bind_end__
.. code-block::
hello
Here is another example that passes the same message to both actors, which can then execute in parallel.
It uses :class:`ray.dag.MultiOutputNode <ray.dag.output_node.MultiOutputNode>` to indicate that this DAG returns multiple outputs.
Then, :func:`dag.execute() <ray.dag.compiled_dag_node.CompiledDAG.execute>` returns multiple :class:`CompiledDAGRef <ray.experimental.compiled_dag_ref.CompiledDAGRef>` objects, one per node:
.. literalinclude:: ../doc_code/cgraph_quickstart.py
:language: python
:start-after: __cgraph_multi_output_start__
:end-before: __cgraph_multi_output_end__
.. code-block::
Execution takes 86.72196418046951 us
Be aware that:
* On the same actor, a Compiled Graph executes in order. If an actor has multiple tasks in the same Compiled Graph, it executes all of them to completion before executing on the next DAG input.
* Across actors in the same Compiled Graph, the execution may be pipelined. An actor may begin executing on the next DAG input while a downstream actor executes on the current one.
* Compiled Graphs currently only supports actor tasks. Non-actor tasks aren't supported.
``asyncio`` support
-------------------
If your Compiled Graph driver is running in an ``asyncio`` event loop, use the ``async`` APIs to ensure that executing
the Compiled Graph and getting the results doesn't block the event loop.
First, pass ``enable_async=True`` to the ``dag.experimental_compile()``:
.. literalinclude:: ../doc_code/cgraph_quickstart.py
:language: python
:start-after: __cgraph_async_compile_start__
:end-before: __cgraph_async_compile_end__
Next, use `execute_async` to invoke the Compiled Graph. Calling ``await`` on ``execute_async`` will return once
the input has been submitted, and it returns a future that can be used to get the result. Finally,
use `await` to get the result of the Compiled Graph.
.. literalinclude:: ../doc_code/cgraph_quickstart.py
:language: python
:start-after: __cgraph_async_execute_start__
:end-before: __cgraph_async_execute_end__
Execution and failure semantics
-------------------------------
Like classic Ray Core, Ray Compiled Graph propagates exceptions to the final output.
In particular:
- **Application exceptions**: If an application task throws an exception, Compiled Graph
wraps the exception in a :class:`RayTaskError <ray.exceptions.RayTaskError>` and
raises it when the caller calls :func:`ray.get() <ray.get>` on the result. The thrown
exception inherits from both :class:`RayTaskError <ray.exceptions.RayTaskError>`
and the original exception class.
- **System exceptions**: System exceptions include actor death or unexpected errors
such as network errors. For actor death, Compiled Graph raises a
:class:`ActorDiedError <ray.exceptions.ActorDiedError>`, and for other errors, it
raises a :class:`RayChannelError <ray.exceptions.RayChannelError>`.
The graph can still execute after application exceptions. However, the graph
automatically shuts down in the case of system exceptions. If an actor's death causes
the graph to shut down, the remaining actors stay alive.
For example, this example explicitly destroys an actor while it's participating in a Compiled Graph.
The remaining actors are reusable:
.. literalinclude:: ../doc_code/cgraph_quickstart.py
:language: python
:start-after: __cgraph_actor_death_start__
:end-before: __cgraph_actor_death_end__
Execution Timeouts
------------------
Some errors, such as NCCL network errors, require additional handling to avoid hanging.
In the future, Ray may attempt to detect such errors, but currently as a fallback, it allows
configurable timeouts for
:func:`compiled_dag.execute() <ray.dag.compiled_dag_node.CompiledDAG.execute>` and :func:`ray.get() <ray.get>`.
The default timeout is 10 seconds for both. Set the following environment variables
to change the default timeout:
- ``RAY_CGRAPH_submit_timeout``: Timeout for :func:`compiled_dag.execute() <ray.dag.compiled_dag_node.CompiledDAG.execute>`.
- ``RAY_CGRAPH_get_timeout``: Timeout for :func:`ray.get() <ray.get>`.
:func:`ray.get() <ray.get>` also has a timeout parameter to set timeout on a per-call basis.
CPU to GPU communication
------------------------
With classic Ray Core, passing ``torch.Tensors`` between actors can become expensive, especially
when transferring between devices. This is because Ray Core doesn't know the final destination device.
Therefore, you may see unnecessary copies across devices other than the source and destination devices.
Ray Compiled Graph ships with native support for passing ``torch.Tensors`` between actors executing on different
devices. Developers can now use type hint annotations in the Compiled Graph declaration to indicate the final
destination device of a ``torch.Tensor``.
.. literalinclude:: ../doc_code/cgraph_nccl.py
:language: python
:start-after: __cgraph_cpu_to_gpu_actor_start__
:end-before: __cgraph_cpu_to_gpu_actor_end__
In Ray Core, if you try to pass a CPU tensor from the driver,
the GPU actor receives a CPU tensor:
.. testcode::
:skipif: True
# This will fail because the driver passes a CPU copy of the tensor,
# and the GPU actor also receives a CPU copy.
ray.get(actor.process.remote(torch.zeros(10)))
With Ray Compiled Graph, you can annotate DAG nodes with type hints to indicate that there may be a ``torch.Tensor``
contained in the value:
.. literalinclude:: ../doc_code/cgraph_nccl.py
:language: python
:start-after: __cgraph_cpu_to_gpu_start__
:end-before: __cgraph_cpu_to_gpu_end__
Under the hood, the Ray Compiled Graph backend copies the ``torch.Tensor`` to the GPU assigned to the ``GPUActor`` by Ray Core.
Of course, you can also do this yourself, but there are advantages to using Compiled Graph instead:
- Ray Compiled Graph can minimize the number of data copies made. For example, passing from one CPU to
multiple GPUs requires one copy to a shared memory buffer, and then one host-to-device copy per
destination GPU.
- In the future, this can be further optimized through techniques such as
`memory pinning <https://pytorch.org/docs/stable/generated/torch.Tensor.pin_memory.html>`_,
using zero-copy deserialization when the CPU is the destination, etc.
GPU to GPU communication
------------------------
Ray Compiled Graphs supports NCCL-based transfers of CUDA ``torch.Tensor`` objects, avoiding any copies through Ray's CPU-based shared-memory object store.
With user-provided type hints, Ray prepares NCCL communicators and
operation scheduling ahead of time, avoiding deadlock and :ref:`overlapping compute and communication <compiled-graph-overlap>`.
Ray Compiled Graph uses `cupy <https://cupy.dev/>`_ under the hood to support NCCL operations.
The cupy version affects the NCCL version. The Ray team is also planning to support custom communicators in the future, for example to support collectives across CPUs or to reuse existing collective groups.
First, create sender and receiver actors. Note that this example requires at least 2 GPUs.
.. literalinclude:: ../doc_code/cgraph_nccl.py
:language: python
:start-after: __cgraph_nccl_setup_start__
:end-before: __cgraph_nccl_setup_end__
To support GPU-to-GPU communication with NCCL, wrap the DAG node that contains the ``torch.Tensor`` that you want to transmit using the ``with_tensor_transport`` API hint:
.. literalinclude:: ../doc_code/cgraph_nccl.py
:language: python
:start-after: __cgraph_nccl_exec_start__
:end-before: __cgraph_nccl_exec_end__
Current limitations include:
* ``torch.Tensor`` and NVIDIA NCCL only
* Support for peer-to-peer transfers. Collective communication operations are coming soon.
* Communication operations are currently done synchronously. :ref:`Overlapping compute and communication <compiled-graph-overlap>` is an experimental feature.
@@ -0,0 +1,84 @@
.. _ray-compiled-graph:
Ray Compiled Graph (beta)
=========================
.. warning::
Ray Compiled Graph is currently in beta (since Ray 2.44). The APIs are subject to change and expected to evolve.
The API is available from Ray 2.32, but it's recommended to use a version after 2.44.
As large language models (LLMs) become common, programming distributed systems with multiple GPUs is essential.
:ref:`Ray Core APIs <core-key-concepts>` facilitate using multiple GPUs but have limitations such as:
* System overhead of ~1 ms per task launch, which is unsuitable for high-performance tasks like LLM inference.
* Lack of support for direct GPU-to-GPU communication, requiring manual development with external libraries like NVIDIA Collective Communications Library (`NCCL <https://developer.nvidia.com/nccl>`_).
Ray Compiled Graph gives you a Ray Core-like API but with:
- **Less than 50us system overhead** for workloads that repeatedly execute the same task graph.
- **Native support for GPU-GPU communication** with NCCL.
For example, consider the following Ray Core code, which sends data to an actor
and gets the result:
.. testcode::
:skipif: True
# Ray Core API for remote execution.
# ~1ms overhead to invoke `recv`.
ref = receiver.recv.remote(data)
ray.get(ref)
This code shows how to compile and execute the same example as a Compiled Graph.
.. testcode::
:skipif: True
# Compiled Graph for remote execution.
# less than 50us overhead to invoke `recv` (during `graph.execute(data)`).
with InputNode() as inp:
graph = receiver.recv.bind(inp)
graph = graph.experimental_compile()
ref = graph.execute(data)
ray.get(ref)
Ray Compiled Graph has a static execution model. It's different from classic Ray APIs, which are eager. Because
of the static nature, Ray Compiled Graph can perform various optimizations such as:
- Pre-allocate resources so that it can reduce system overhead.
- Prepare NCCL communicators and apply deadlock-free scheduling.
- (experimental) Automatically overlap GPU compute and communication.
- Improve multi-node performance.
Use Cases
---------
Ray Compiled Graph APIs simplify development of high-performance multi-GPU workloads such as LLM inference or distributed training that require:
- Sub-millisecond level task orchestration.
- Direct GPU-GPU peer-to-peer or collective communication.
- `Heterogeneous <https://www.youtube.com/watch?v=Mg08QTBILWU>`_ or MPMD (Multiple Program Multiple Data) execution.
More Resources
--------------
- `Ray Compiled Graph blog <https://www.anyscale.com/blog/announcing-compiled-graphs>`_
- `Ray Compiled Graph talk at Ray Summit <https://www.youtube.com/watch?v=jv58Cpr6SAs>`_
- `Heterogeneous training with Ray Compiled Graph <https://www.youtube.com/watch?v=Mg08QTBILWU>`_
- `Distributed LLM inference with Ray Compiled Graph <https://www.youtube.com/watch?v=oMb_WiUwf5o>`_
Table of Contents
-----------------
Learn more details about Ray Compiled Graph from the following links.
.. toctree::
:maxdepth: 1
quickstart
profiling
overlap
troubleshooting
compiled-graph-api
@@ -0,0 +1,99 @@
Troubleshooting
===============
This page contains common issues and solutions for Compiled Graph execution.
Limitations
-----------
Compiled Graph is a new feature and has some limitations:
- Invoking Compiled Graph
- Only the process that compiles the Compiled Graph may call it.
- A Compiled Graph has a maximum number of in-flight executions. When using the DAG API,
if there aren't enough resources at the time of ``dag.execute()``, Ray will queue the
tasks for later execution. Ray Compiled Graph currently doesn't support queuing past its
maximum capacity. Therefore, you may need to consume some results using ``ray.get()``
before submitting more executions. As a stopgap,
``dag.execute()`` throws a ``RayCgraphCapacityExceeded`` exception if the call takes too long.
In the future, Compiled Graph may have better error handling and queuing.
- Compiled Graph Execution
- Ideally, you should try not to execute other tasks on the actor while it is participating in a Compiled Graph.
Compiled Graph tasks will be executed on a **background thread**. Any concurrent tasks
submitted to the actor can still execute on the main thread, but you are responsible for
synchronization with the Compiled Graph background thread.
- For now, actors can only execute one Compiled Graph at a time. To execute a different Compiled Graph
on the same actor, you must teardown the current Compiled Graph.
See :ref:`Return NumPy arrays <troubleshoot-numpy>` for more details.
- Passing and getting Compiled Graph results (:class:`CompiledDAGRef <ray.experimental.compiled_dag_ref.CompiledDAGRef>`)
- Compiled Graph results can't be passed to another task or actor. This restriction may be loosened
in the future, but for now, it allows for better performance because the backend knows
exactly where to push the results.
- ``ray.get()`` can be called at most once on a :class:`CompiledDAGRef <ray.experimental.compiled_dag_ref.CompiledDAGRef>`. An exception will be raised if
it is called twice on the same :class:`CompiledDAGRef <ray.experimental.compiled_dag_ref.CompiledDAGRef>`. This is because the underlying memory for
the result may need to be reused for a future DAG execution. Restricting ``ray.get()`` to once
per reference simplifies the tracking of the memory buffers.
- If the value returned by ``ray.get()`` is zero-copy deserialized, then subsequent executions
of the same DAG will block until the value goes out of scope in Python. Thus, if you hold onto
zero-copy deserialized values returned by ``ray.get()``, and you try to execute the Compiled Graph above
its max concurrency, it may deadlock. This case will be detected in the future, but for now
you will receive a ``RayChannelTimeoutError``.
See :ref:`Explicitly teardown before reusing the same actors <troubleshoot-teardown>`
for more details.
- Collective operations
- For GPU to GPU communication, Compiled Graph only supports peer-to-peer transfers. Collective communication operations are coming soon.
Keep an eye out for additional features in future Ray releases:
- Support better queuing of DAG inputs, to enable more concurrent executions of the same DAG.
- Support for more collective operations with NCCL.
- Support for multiple DAGs executing on the same actor.
- General performance improvements.
If you run into additional issues, or have other feedback or questions, file an issue
on `GitHub <https://github.com/ray-project/ray/issues>`_.
For a full list of known issues, check the ``compiled-graphs`` label on Ray GitHub.
.. _troubleshoot-numpy:
Returning NumPy arrays
----------------------
Ray zero-copy deserializes NumPy arrays when possible. If you execute compiled graph with a NumPy array output multiple times,
you could possibly run into issues if a NumPy array output from a previous Compiled Graph execution isn't deleted before attempting to get the result
of a following execution of the same Compiled Graph. This is because the NumPy array stays in the buffer of the Compiled Graph until you or Python delete it.
It's recommended to explicitly delete the NumPy array as Python may not always garbage collect the NumPy array immediately as you may expect.
For example, the following code sample could result in a hang or RayChannelTimeoutError if the NumPy array isn't deleted:
.. literalinclude:: ../doc_code/cgraph_troubleshooting.py
:language: python
:start-after: __numpy_troubleshooting_start__
:end-before: __numpy_troubleshooting_end__
In the preceding code snippet, Python may not garbage collect the NumPy array in `result` on each iteration of the loop.
Therefore, you should explicitly delete the NumPy array before you try to get the result of subsequent Compiled Graph executions.
.. _troubleshoot-teardown:
Explicitly teardown before reusing the same actors
--------------------------------------------------
If you want to reuse the actors of a Compiled Graph, it's important to explicitly teardown the Compiled Graph before reusing the actors.
Without explicitly tearing down the Compiled Graph, the resources created for actors in a Compiled Graph may have conflicts with further usage of those actors.
For example, in the following code, Python could delay garbage collection, which triggers the implicit teardown of the first Compiled Graph. This could lead to a segfault due to the resource conflicts mentioned:
.. literalinclude:: ../doc_code/cgraph_troubleshooting.py
:language: python
:start-after: __teardown_troubleshooting_start__
:end-before: __teardown_troubleshooting_end__
+347
View File
@@ -0,0 +1,347 @@
.. _configuring-ray:
Configuring Ray
===============
.. note:: For running Java applications, see `Java Applications`_.
This page discusses the various ways to configure Ray, both from the Python API
and from the command line. Take a look at the ``ray.init`` `documentation
<package-ref.html#ray.init>`__ for a complete overview of the configurations.
.. important:: For the multi-node setting, you must first run ``ray start`` on the command line to start the Ray cluster services on the machine before ``ray.init`` in Python to connect to the cluster services. On a single machine, you can run ``ray.init()`` without ``ray start``, which both starts the Ray cluster services and connects to them.
.. _cluster-resources:
Cluster resources
-----------------
Ray by default detects available resources.
.. testcode::
:hide:
import ray
ray.shutdown()
.. testcode::
import ray
# This automatically detects available resources in the single machine.
ray.init()
If not running cluster mode, you can specify cluster resources overrides through ``ray.init`` as follows.
.. testcode::
:hide:
ray.shutdown()
.. testcode::
# If not connecting to an existing cluster, you can specify resources overrides:
ray.init(num_cpus=8, num_gpus=1)
.. testcode::
:hide:
ray.shutdown()
.. testcode::
# Specifying custom resources
ray.init(num_gpus=1, resources={'Resource1': 4, 'Resource2': 16})
When starting Ray from the command line, pass the ``--num-cpus`` and ``--num-gpus`` flags into ``ray start``. You can also specify custom resources.
.. code-block:: bash
# To start a head node.
$ ray start --head --num-cpus=<NUM_CPUS> --num-gpus=<NUM_GPUS>
# To start a non-head node.
$ ray start --address=<address> --num-cpus=<NUM_CPUS> --num-gpus=<NUM_GPUS>
# Specifying custom resources
ray start [--head] --num-cpus=<NUM_CPUS> --resources='{"Resource1": 4, "Resource2": 16}'
If using the command line, connect to the Ray cluster as follow:
.. testcode::
:skipif: True
# Connect to ray. Notice if connected to existing cluster, you don't specify resources.
ray.init(address=<address>)
.. _temp-dir-log-files:
Logging and debugging
---------------------
Each Ray session has a unique name. By default, the name is
``session_{timestamp}_{pid}``. The format of ``timestamp`` is
``%Y-%m-%d_%H-%M-%S_%f`` (See `Python time format <strftime.org>`__ for details);
the pid belongs to the startup process (the process calling ``ray.init()`` or
the Ray process executed by a shell in ``ray start``).
For each session, Ray places all its temporary files under the
*session directory*. A *session directory* is a subdirectory of the
*root temporary path* (``/tmp/ray`` by default),
so the default session directory is ``/tmp/ray/{ray_session_name}``.
You can sort by their names to find the latest session.
Change the *root temporary directory* by passing ``--temp-dir={your temp path}`` to ``ray start``.
There currently isn't a stable way to change the root temporary directory when calling ``ray.init()``, but if you need to, you can provide the ``_temp_dir`` argument to ``ray.init()``.
See :ref:`Logging Directory Structure <logging-directory-structure>` for more details.
.. _ray-ports:
Ports configurations
--------------------
Ray requires bi-directional communication among its nodes in a cluster. Each node opens specific ports to receive incoming network requests.
All Nodes
~~~~~~~~~
- ``--node-manager-port``: Raylet port for node manager. Default: Random value.
- ``--object-manager-port``: Raylet port for object manager. Default: Random value.
- ``--runtime-env-agent-port``: Raylet port for runtime env agent. Default: Random value.
The node manager and object manager run as separate processes with their own ports for communication.
The following options specify the ports used by dashboard agent process.
- ``--dashboard-agent-grpc-port``: The port to listen for grpc on. Default: Random value.
- ``--dashboard-agent-listen-port``: The port to listen for http on. Default: 52365.
- ``--metrics-export-port``: The port to use to expose Ray metrics. Default: Random value.
The following options specify the range of ports used by worker processes across machines. All ports in the range should be open.
- ``--min-worker-port``: Minimum port number for the worker to bind to. Default: 10002.
- ``--max-worker-port``: Maximum port number for the worker to bind to. Default: 19999.
Port numbers are how Ray differentiates input and output to and from multiple workers on a single node. Each worker takes input and gives output on a single port number. Therefore, by default, there's a maximum of 10,000 workers on each node, irrespective of number of CPUs.
In general, you should give Ray a wide range of possible worker ports, in case any of those ports happen to be in use by some other program on your machine. However, when debugging, it's useful to explicitly specify a short list of worker ports such as ``--worker-port-list=10000,10001,10002,10003,10004``
Note that this practice limits the number of workers, just like specifying a narrow range.
Head node
~~~~~~~~~
In addition to ports specified in the preceding section, the head node needs to open several more ports.
- ``--port``: Port of the Ray GCS server. The head node starts a GCS server listening on this port. Default: 6379.
- ``--ray-client-server-port``: Listening port for Ray Client Server. Default: 10001.
- ``--redis-shard-ports``: Comma-separated list of ports for non-primary Redis shards. Default: Random values.
- ``--dashboard-grpc-port``: (Deprecated) No longer used. Only kept for backward compatibility.
- If ``--include-dashboard`` is true (the default), then the head node must open ``--dashboard-port``. Default: 8265.
If ``--include-dashboard`` is true but the ``--dashboard-port`` isn't open on
the head node, you won't be able to access the dashboard, and you repeatedly get
.. code-block:: bash
WARNING worker.py:1114 -- The agent on node <hostname of node that tried to run a task> failed with the following error:
Traceback (most recent call last):
File "/usr/local/lib/python3.8/dist-packages/grpc/aio/_call.py", line 285, in __await__
raise _create_rpc_error(self._cython_call._initial_metadata,
grpc.aio._call.AioRpcError: <AioRpcError of RPC that terminated with:
status = StatusCode.UNAVAILABLE
details = "failed to connect to all addresses"
debug_error_string = "{"description":"Failed to pick subchannel","file":"src/core/ext/filters/client_channel/client_channel.cc","file_line":4165,"referenced_errors":[{"description":"failed to connect to all addresses","file":"src/core/ext/filters/client_channel/lb_policy/pick_first/pick_first.cc","file_line":397,"grpc_status":14}]}"
If you see that error, check whether the ``--dashboard-port`` is accessible
through ``nc``, ``nmap``, or your hello browser.
.. code-block:: bash
$ nmap -sV --reason -p 8265 $HEAD_ADDRESS
Nmap scan report for compute04.berkeley.edu (123.456.78.910)
Host is up, received reset ttl 60 (0.00065s latency).
rDNS record for 123.456.78.910: compute04.berkeley.edu
PORT STATE SERVICE REASON VERSION
8265/tcp open http syn-ack ttl 60 aiohttp 3.7.2 (Python 3.8)
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Note that the dashboard runs as a separate subprocess which can crash invisibly
in the background, so even if you checked port 8265 earlier, the port might be
closed *now* (for the prosaic reason that there's no longer a service running
on it). This also means that if you ``ray stop`` and ``ray start`` when the port is
unreachable, it may become reachable again due to the dashboard restarting.
If you don't want the dashboard, set ``--include-dashboard=false``.
TLS authentication
------------------
You can configure Ray to use TLS on its gRPC channels.
This means that connecting to the Ray head requires
an appropriate set of credentials and also that data exchanged between
various processes (client, head, workers) is encrypted.
TLS uses the private key and public key for encryption and decryption. The owner
keeps the private key secret and TLS shares the public key with the other party.
This pattern ensures that only the intended recipient can read the message.
A Certificate Authority (CA) is a trusted third party that certifies the identity of the
public key owner. The digital certificate issued by the CA contains the public key itself,
the identity of the public key owner, and the expiration date of the certificate. Note that
if the owner of the public key doesn't want to obtain a digital certificate from a CA,
they can generate a self-signed certificate with tools like OpenSSL.
To obtain a digital certificate, the owner of the public key must generate a Certificate Signing
Request (CSR). The CSR contains information about the owner of the public
key and the public key itself. Ray requires additional steps for achieving
a successful TLS encryption.
Here is a step-by-step guide for adding TLS Authentication to a static Kubernetes Ray cluster using
a self-signed certificates:
Step 1: Generate a private key and self-signed certificate for CA
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: bash
openssl req -x509 \
-sha256 -days 3650 \
-nodes \
-newkey rsa:2048 \
-subj "/CN=*.ray.io/C=US/L=San Francisco" \
-keyout ca.key -out ca.crt
Use the following command to encode the private key file and the self-signed certificate file,
then paste encoded strings to the secret.yaml.
.. code-block:: bash
cat ca.key | base64
cat ca.crt | base64
# Alternatively, this command automatically encode and create the secret for the CA key pair.
.. code-block:: bash
kubectl create secret generic ca-tls --from-file=ca.crt=<path-to-ca.crt> --from-file=ca.key=<path-to-ca.key>
Step 2: Generate individual private keys and self-signed certificates for the Ray head and workers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The `YAML file
<https://raw.githubusercontent.com/ray-project/ray/master/doc/source/cluster/kubernetes/configs/static-ray-cluster.tls.yaml>`__, has a ConfigMap named `tls` that
includes two shell scripts: `gencert_head.sh` and `gencert_worker.sh`. These scripts produce the private key
and self-signed certificate files (`tls.key` and `tls.crt`) for both head and worker Pods in the initContainer
of each deployment. By using the initContainer, we can dynamically retrieve the `POD_IP` to the `[alt_names]` section.
The scripts perform the following steps: first, it generates a 2048-bit RSA private key and saves the key as
`/etc/ray/tls/tls.key`. Then, a Certificate Signing Request (CSR) is generated using the `tls.key` file
and the `csr.conf` configuration file. Finally, a self-signed certificate (`tls.crt`) is created using
the Certificate Authority's (`ca.key and ca.crt`) keypair and the CSR (`ca.csr`).
Step 3: Set the environment variables for both Ray head and worker to enable TLS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You enable TLS by setting environment variables.
- ``RAY_USE_TLS``: Either 1 or 0 to use/not-use TLS. If you set it to 1, you must set the environment variables below. Default: 0.
- ``RAY_TLS_SERVER_CERT``: Location of a `certificate file (tls.crt)`, which Ray presents to other endpoints to achieve mutual authentication.
- ``RAY_TLS_SERVER_KEY``: Location of a `private key file (tls.key)`, which is the cryptographic means to prove to other endpoints that you are the authorized user of a given certificate.
- ``RAY_TLS_CA_CERT``: Location of a `CA certificate file (ca.crt)`, which allows TLS to decide whether an the correct authority signed the endpoint's certificate.
Step 4: Verify TLS authentication
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: bash
# Log in to the worker Pod
kubectl exec -it ${WORKER_POD} -- bash
# Since the head Pod has the certificate of the full qualified DNS resolution for the Ray head service, the connection to the worker Pods
# is established successfully
ray health-check --address service-ray-head.default.svc.cluster.local:6379
# Since service-ray-head hasn't added to the alt_names section in the certificate, the connection fails and an error
# message similar to the following is displayed: "Peer name service-ray-head is not in peer certificate".
ray health-check --address service-ray-head:6379
# After you add `DNS.3 = service-ray-head` to the alt_names sections and deploy the YAML again, the connection is able to work.
Enabling TLS causes a performance hit due to the extra overhead of mutual
authentication and encryption.
Testing has shown that this overhead is large for small workloads and becomes
relatively smaller for large workloads.
The exact overhead depends on the nature of your workload.
Java applications
-----------------
.. important:: For the multi-node setting, you must first run ``ray start`` on the command line to start the Ray cluster services on the machine before ``ray.init()`` in Java to connect to the cluster services. On a single machine, you can run ``ray.init()`` without ``ray start``. It both starts the Ray cluster services and connects to them.
.. _code_search_path:
Code search path
~~~~~~~~~~~~~~~~
If you want to run a Java application in a multi-node cluster, you must specify the code search path in your driver. The code search path tells Ray where to load jars when starting Java workers. You must distribute your jar files to the same paths on all nodes of the Ray cluster before running your code.
.. code-block:: bash
$ java -classpath <classpath> \
-Dray.address=<address> \
-Dray.job.code-search-path=/path/to/jars/ \
<classname> <args>
The ``/path/to/jars/`` points to a directory which contains jars. Workers load all jars in the directory. You can also provide multiple directories for this parameter.
.. code-block:: bash
$ java -classpath <classpath> \
-Dray.address=<address> \
-Dray.job.code-search-path=/path/to/jars1:/path/to/jars2:/path/to/pys1:/path/to/pys2 \
<classname> <args>
You don't need to configure code search path if you run a Java application in a single-node cluster.
See ``ray.job.code-search-path`` under :ref:`Driver Options <java-driver-options>` for more information.
.. note:: Currently there's no way to configure Ray when running a Java application in single machine mode. If you need to configure Ray, run ``ray start`` to start the Ray cluster first.
.. _java-driver-options:
Driver options
~~~~~~~~~~~~~~
There's a limited set of options for Java drivers. They're not for configuring the Ray cluster, but only for configuring the driver.
Ray uses `Typesafe Config <https://lightbend.github.io/config/>`__ to read options. There are several ways to set options:
- System properties. You can configure system properties either by adding options in the format of ``-Dkey=value`` in the driver command line, or by invoking ``System.setProperty("key", "value");`` before ``Ray.init()``.
- A `HOCON format <https://github.com/lightbend/config/blob/master/HOCON.md>`__ configuration file. By default, Ray will try to read the file named ``ray.conf`` in the root of the classpath. You can customize the location of the file by setting system property ``ray.config-file`` to the path of the file.
.. note:: Options configured by system properties have higher priority than options configured in the configuration file.
The list of available driver options:
- ``ray.address``
- The cluster address if the driver connects to an existing Ray cluster. If it's empty, Ray creates a new Ray cluster.
- Type: ``String``
- Default: empty string.
- ``ray.job.code-search-path``
- The paths for Java workers to load code from. Currently, Ray only supports directories. You can specify one or more directories split by a ``:``. You don't need to configure code search path if you run a Java application in single machine mode or local mode. Ray also uses the code search path to load Python code, if specified. This parameter is required for :ref:`cross_language`. If you specify a code search path, you can only run Python remote functions which you can find in the code search path.
- Type: ``String``
- Default: empty string.
- Example: ``/path/to/jars1:/path/to/jars2:/path/to/pys1:/path/to/pys2``
- ``ray.job.namespace``
- The namespace of this job. Ray uses it for isolation between jobs. Jobs in different namespaces can't access each other. If it's not specified, Ray uses a randomized value.
- Type: ``String``
- Default: A random UUID string value.
.. _`Apache Arrow`: https://arrow.apache.org/
+293
View File
@@ -0,0 +1,293 @@
.. _cross_language:
Cross-language programming
==========================
This page shows you how to use Ray's cross-language programming feature.
Setup the driver
-----------------
You need to set :ref:`code_search_path` in your driver.
.. tab-set::
.. tab-item:: Python
.. literalinclude:: ./doc_code/cross_language.py
:language: python
:start-after: __crosslang_init_start__
:end-before: __crosslang_init_end__
.. tab-item:: Java
.. code-block:: bash
java -classpath <classpath> \
-Dray.address=<address> \
-Dray.job.code-search-path=/path/to/code/ \
<classname> <args>
You may want to include multiple directories to load both Python and Java code for workers, if you place them in different directories.
.. tab-set::
.. tab-item:: Python
.. literalinclude:: ./doc_code/cross_language.py
:language: python
:start-after: __crosslang_multidir_start__
:end-before: __crosslang_multidir_end__
.. tab-item:: Java
.. code-block:: bash
java -classpath <classpath> \
-Dray.address=<address> \
-Dray.job.code-search-path=/path/to/jars:/path/to/pys \
<classname> <args>
Python calling Java
-------------------
Suppose you have a Java static method and a Java class as follows:
.. code-block:: java
package io.ray.demo;
public class Math {
public static int add(int a, int b) {
return a + b;
}
}
.. code-block:: java
package io.ray.demo;
// A regular Java class.
public class Counter {
private int value = 0;
public int increment() {
this.value += 1;
return this.value;
}
}
Then, in Python, you can call the preceding Java remote function, or create an actor
from the preceding Java class.
.. literalinclude:: ./doc_code/cross_language.py
:language: python
:start-after: __python_call_java_start__
:end-before: __python_call_java_end__
Java calling Python
-------------------
Suppose you have a Python module as follows:
.. literalinclude:: ./doc_code/cross_language.py
:language: python
:start-after: __python_module_start__
:end-before: __python_module_end__
.. note::
* You should decorate the function or class with `@ray.remote`.
Then, in Java, you can call the preceding Python remote function, or create an actor
from the preceding Python class.
.. code-block:: java
package io.ray.demo;
import io.ray.api.ObjectRef;
import io.ray.api.PyActorHandle;
import io.ray.api.Ray;
import io.ray.api.function.PyActorClass;
import io.ray.api.function.PyActorMethod;
import io.ray.api.function.PyFunction;
import org.testng.Assert;
public class JavaCallPythonDemo {
public static void main(String[] args) {
// Set the code-search-path to the directory of your `ray_demo.py` file.
System.setProperty("ray.job.code-search-path", "/path/to/the_dir/");
Ray.init();
// Define a Python class.
PyActorClass actorClass = PyActorClass.of(
"ray_demo", "Counter");
// Create a Python actor and call actor method.
PyActorHandle actor = Ray.actor(actorClass).remote();
ObjectRef objRef1 = actor.task(
PyActorMethod.of("increment", int.class)).remote();
Assert.assertEquals(objRef1.get(), 1);
ObjectRef objRef2 = actor.task(
PyActorMethod.of("increment", int.class)).remote();
Assert.assertEquals(objRef2.get(), 2);
// Call the Python remote function.
ObjectRef objRef3 = Ray.task(PyFunction.of(
"ray_demo", "add", int.class), 1, 2).remote();
Assert.assertEquals(objRef3.get(), 3);
Ray.shutdown();
}
}
Cross-language data serialization
---------------------------------
Ray automatically serializes and deserializes the arguments and return values of Ray calls
if their types are the following:
- Primitive data types
=========== ======= =======
MessagePack Python Java
=========== ======= =======
nil None null
bool bool Boolean
int int Short / Integer / Long / BigInteger
float float Float / Double
str str String
bin bytes byte[]
=========== ======= =======
- Basic container types
=========== ======= =======
MessagePack Python Java
=========== ======= =======
array list Array
=========== ======= =======
- Ray builtin types
- ActorHandle
.. note::
* Be aware of float / double precision between Python and Java. If Java is using a
float type to receive the input argument, the double precision Python data
reduces to float precision in Java.
* BigInteger can support a max value of 2^64-1. See:
https://github.com/msgpack/msgpack/blob/master/spec.md#int-format-family.
If the value is larger than 2^64-1, then sending the value to Python raises an exception.
The following example shows how to pass these types as parameters and how to
return these types.
You can write a Python function which returns the input data:
.. literalinclude:: ./doc_code/cross_language.py
:language: python
:start-after: __serialization_start__
:end-before: __serialization_end__
Then you can transfer the object from Java to Python, and back from Python
to Java:
.. code-block:: java
package io.ray.demo;
import io.ray.api.ObjectRef;
import io.ray.api.Ray;
import io.ray.api.function.PyFunction;
import java.math.BigInteger;
import org.testng.Assert;
public class SerializationDemo {
public static void main(String[] args) {
Ray.init();
Object[] inputs = new Object[]{
true, // Boolean
Byte.MAX_VALUE, // Byte
Short.MAX_VALUE, // Short
Integer.MAX_VALUE, // Integer
Long.MAX_VALUE, // Long
BigInteger.valueOf(Long.MAX_VALUE), // BigInteger
"Hello World!", // String
1.234f, // Float
1.234, // Double
"example binary".getBytes()}; // byte[]
for (Object o : inputs) {
ObjectRef res = Ray.task(
PyFunction.of("ray_serialization", "py_return_input", o.getClass()),
o).remote();
Assert.assertEquals(res.get(), o);
}
Ray.shutdown();
}
}
Cross-language exception stacks
-------------------------------
Suppose you have a Java package as follows:
.. code-block:: java
package io.ray.demo;
import io.ray.api.ObjectRef;
import io.ray.api.Ray;
import io.ray.api.function.PyFunction;
public class MyRayClass {
public static int raiseExceptionFromPython() {
PyFunction<Integer> raiseException = PyFunction.of(
"ray_exception", "raise_exception", Integer.class);
ObjectRef<Integer> refObj = Ray.task(raiseException).remote();
return refObj.get();
}
}
and a Python module as follows:
.. literalinclude:: ./doc_code/cross_language.py
:language: python
:start-after: __raise_exception_start__
:end-before: __raise_exception_end__
Then, run the following code:
.. literalinclude:: ./doc_code/cross_language.py
:language: python
:start-after: __raise_exception_demo_start__
:end-before: __raise_exception_demo_end__
The exception stack will be:
.. code-block:: text
Traceback (most recent call last):
File "ray_exception_demo.py", line 9, in <module>
ray.get(obj_ref) # <-- raise exception from here.
File "ray/python/ray/_private/client_mode_hook.py", line 105, in wrapper
return func(*args, **kwargs)
File "ray/python/ray/_private/worker.py", line 2247, in get
raise value
ray.exceptions.CrossLanguageError: An exception raised from JAVA:
io.ray.api.exception.RayTaskException: (pid=61894, ip=172.17.0.2) Error executing task c8ef45ccd0112571ffffffffffffffffffffffff01000000
at io.ray.runtime.task.TaskExecutor.execute(TaskExecutor.java:186)
at io.ray.runtime.RayNativeRuntime.nativeRunTaskExecutor(Native Method)
at io.ray.runtime.RayNativeRuntime.run(RayNativeRuntime.java:231)
at io.ray.runtime.runner.worker.DefaultWorker.main(DefaultWorker.java:15)
Caused by: io.ray.api.exception.CrossLanguageException: An exception raised from PYTHON:
ray.exceptions.RayTaskError: ray::raise_exception() (pid=62041, ip=172.17.0.2)
File "ray_exception.py", line 7, in raise_exception
1 / 0
ZeroDivisionError: division by zero
@@ -0,0 +1,207 @@
.. _custom-tensor-transport:
*************************************************
Implementing a custom tensor transport (Advanced)
*************************************************
Ray Direct Transport (RDT) allows you to register custom tensor transports at runtime.
This page explains how to implement a custom tensor transport by implementing the :class:`TensorTransportManager <ray.experimental.TensorTransportManager>` abstract interface.
Overview
========
To create a custom tensor transport:
1. Implement the abstract interface :class:`ray.experimental.TensorTransportManager <ray.experimental.TensorTransportManager>`.
2. Define custom metadata classes by extending :class:`TensorTransportMetadata <ray.experimental.TensorTransportMetadata>` and :class:`CommunicatorMetadata <ray.experimental.CommunicatorMetadata>`.
3. Register your transport using :func:`ray.experimental.register_tensor_transport <ray.experimental.register_tensor_transport>`.
When Ray needs to transfer a tensor between actors using your transport, it calls specific methods on your ``TensorTransportManager`` implementation at different stages of the transfer lifecycle.
Implementing TensorTransportManager
===================================
The :class:`TensorTransportManager <ray.experimental.TensorTransportManager>` abstract class defines the interface for custom tensor transports. You must implement all abstract methods.
The following diagram shows when each method is called during a tensor transfer:
.. code-block:: text
Source Actor Owner Process Destination Actor
============ ============= =================
| | |
1. Task returns tensor | |
``extract_tensor_transport_metadata`` |
| | |
| ---- transport_metadata ----> | |
| | |
| 2. Prepare communicator |
| ``get_communicator_metadata`` |
| | |
| <---- comm metadata --------- | ---- comm metadata --------> |
| | |
3. ``send_multiple_tensors`` | 3. ``recv_multiple_tensors``
| |
| ------------ tensors ---------------------------------------> |
| | |
| (transfer complete) |
| | |
| 5. Ref goes out of scope |
| <---------------------------- | |
5. Clean up resources | |
``garbage_collect`` | |
Note that Ray will not call `send_multiple_tensors` for one-sided transports.
The following diagram shows where each method is called in the ray.put / ray.get case supported by one-sided transports.
.. code-block:: text
Source Actor Destination Actor
============ =================
| |
1. User ``ray.put``'s tensor |
``extract_tensor_transport_metadata`` |
| |
| |
2. User passes ref to another actor |
| ---- transport_metadata ----------------------------------> |
| |
| |
| 3. User ``ray.get``'s on object ref
``get_communicator_metadata``
| ``recv_multiple_tensors``
| ------------ tensors --------- -----------------------------> |
| |
| (transfer complete) |
| |
4. Clean up resources |
``garbage_collect`` |
(when ref goes out of scope) |
The API reference page for :class:`TensorTransportManager <ray.experimental.TensorTransportManager>` has more details on what each method does and how to implement them.
See implementations of Ray's default transports (NCCL, NIXL, etc.) in the `python/ray/experimental/rdt/ <https://github.com/ray-project/ray/tree/master/python/ray/experimental/rdt>`_ directory.
The following is an walk-through for implementing and using a custom tensor transport.
Example: Shared memory tensor transport
========================================
The following walks through a complete custom tensor transport that transfers ``numpy`` arrays through shared memory.
Note that because shared memory is one-sided (the receiver directly reads the memory block the sender wrote to),
``is_one_sided`` returns ``True`` and Ray never calls ``send_multiple_tensors``.
Define metadata classes
-----------------------
Your transport uses two metadata classes that flow through different stages of the transfer:
- :class:`TensorTransportMetadata <ray.experimental.TensorTransportMetadata>` is created on the **source actor** during ``extract_tensor_transport_metadata``. It carries per-tensor information (shapes, dtypes, devices) plus any transport-specific identifiers (e.g., shared memory block names, RDMA keys) that the receiver needs to locate and read the data.
- :class:`CommunicatorMetadata <ray.experimental.CommunicatorMetadata>` is created on the **owner/driver process** during ``get_communicator_metadata``. It carries any coordination information both actors need, such as ranks in a collective group. For one-sided transports (where the receiver can directly read the sender's memory), an empty metadata object is typically sufficient.
Start by extending these classes to carry any transport-specific state.
``ShmTransportMetadata`` stores the shared memory block name and size so the receiver can locate and read the data.
This transport doesn't need any communicator metadata, so ``ShmCommunicatorMetadata`` is empty.
.. literalinclude:: ../doc_code/direct_transport_custom.py
:language: python
:start-after: __custom_metadata_start__
:end-before: __custom_metadata_end__
Extract tensor transport metadata
---------------------------------
Ray calls ``extract_tensor_transport_metadata`` on the source actor right after the task produces its result tensors.
Record shapes and dtypes, then perform any transport-specific registration. Here, the implementation serializes the tensors
into a shared memory block and records the block name and size in the metadata so the receiver can find it.
.. literalinclude:: ../doc_code/direct_transport_custom.py
:language: python
:start-after: __custom_extract_start__
:end-before: __custom_extract_end__
Get communicator metadata
-------------------------
Ray calls ``get_communicator_metadata`` on the owner/driver process before orchestrating the transfer.
Return any information both actors need to coordinate, such as ranks in a collective group.
For one-sided transports such as shared memory, an empty metadata object is fine.
.. literalinclude:: ../doc_code/direct_transport_custom.py
:language: python
:start-after: __custom_communicator_start__
:end-before: __custom_communicator_end__
Transport properties
--------------------
Define your ``TensorTransportManager`` subclass and implement the property methods.
``tensor_transport_backend`` returns the name that users pass to ``@ray.method(tensor_transport=...)``.
``is_one_sided`` and ``can_abort_transport`` tell Ray how to orchestrate transfers and handle errors.
``actor_has_tensor_transport`` lets Ray check whether a given actor can use this transport.
.. literalinclude:: ../doc_code/direct_transport_custom.py
:language: python
:start-after: __custom_properties_start__
:end-before: __custom_properties_end__
Send and receive
----------------
``recv_multiple_tensors`` runs on the destination actor. For this shared memory transport, it opens the
shared memory block by name and deserializes the tensors.
``send_multiple_tensors`` runs on the source actor for two-sided transports. Since shared memory is one-sided,
Ray never calls this method, so it raises ``NotImplementedError`` as a safety guard.
.. literalinclude:: ../doc_code/direct_transport_custom.py
:language: python
:start-after: __custom_send_recv_start__
:end-before: __custom_send_recv_end__
Cleanup
-------
``garbage_collect`` runs on the source actor when Ray's reference counting determines the object is out of scope.
Release any transport resources here, in this case closing and unlinking the shared memory block.
``abort_transport`` runs on both actors when a system error occurs during transfer, if ``can_abort_transport`` returns ``True``.
Since this transport returns ``False`` for ``can_abort_transport``, Ray kills the involved actors instead,
so ``abort_transport`` is a no-op.
.. literalinclude:: ../doc_code/direct_transport_custom.py
:language: python
:start-after: __custom_cleanup_start__
:end-before: __custom_cleanup_end__
Registering your transport
==========================
After implementing your transport, the **driver process** must register it with :func:`ray.experimental.register_tensor_transport <ray.experimental.register_tensor_transport>` before creating any actors that use it:
.. literalinclude:: ../doc_code/direct_transport_custom.py
:language: python
:start-after: __custom_usage_start__
:end-before: __custom_usage_end__
Limitations
===========
Custom tensor transports have the following limitations:
- **Actor restarts aren't supported.** Your actor doesn't have access to the custom transport after a restart.
- **Register transports before actor creation.** If you register a transport after creating an actor, that actor can't use the new transport.
- **Out-of-order actors** If you have an out-of-order actor (such as an async actor) and the process where you submit the actor task is different from where you created the actor, Ray can't guarantee it has registered your custom transport on the actor at task execution time.
- **Actor creation and task submission from different processes** If the process where you submit an actor task is different from where you created the actor, Ray can't guarantee it has registered your custom transport on the actor at task execution time.
For general RDT limitations, see :ref:`limitations <limitations>`.
Also feel free to reach out through `GitHub issues <https://github.com/ray-project/ray/issues>`_ or the `Ray Slack <https://docs.google.com/forms/d/e/1FAIpQLSfAcoiLCHOguOm8e7Jnn-JJdZaCxPGjgVCvFijHB5PLaQLeig/viewform>`_ to ask any questions.
@@ -0,0 +1,367 @@
.. _direct-transport:
**************************
Ray Direct Transport (RDT)
**************************
Ray objects are normally stored in Ray's CPU-based object store and copied and deserialized when accessed by a Ray task or actor.
For GPU data specifically, this can lead to unnecessary and expensive data transfers.
For example, passing a CUDA ``torch.Tensor`` from one Ray task to another would require a copy from GPU to CPU memory, then back again to GPU memory.
*Ray Direct Transport (RDT)* is a new feature that allows Ray to store and pass objects directly between Ray actors.
This feature augments the familiar Ray :class:`ObjectRef <ray.ObjectRef>` API by:
- Keeping GPU data in GPU memory until a transfer is necessary
- Avoiding expensive serialization and copies to and from the Ray object store
- Using efficient data transports like collective communication libraries (`Gloo <https://github.com/pytorch/gloo>`__ or `NCCL <https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/index.html>`__) or point-to-point RDMA (via `NVIDIA's NIXL <https://github.com/ai-dynamo/nixl>`__) to transfer data directly between devices, including both CPU and GPUs
.. note::
RDT is currently in **alpha** and doesn't support all Ray Core APIs yet. Future releases may introduce breaking API changes. See the :ref:`limitations <limitations>` section for more details.
Getting started
===============
.. tip::
RDT currently supports ``torch.Tensor`` objects created by Ray actor tasks. Other datatypes and Ray non-actor tasks may be supported in future releases.
This walkthrough will show how to create and use RDT with different *tensor transports*, i.e. the mechanism used to transfer the tensor between actors.
Currently, RDT supports the following tensor transports:
1. `Gloo <https://github.com/pytorch/gloo>`__: A collective communication library for PyTorch and CPUs.
2. `NVIDIA NCCL <https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/index.html>`__: A collective communication library for NVIDIA GPUs.
3. `NVIDIA NIXL <https://github.com/ai-dynamo/nixl>`__: A library for accelerating point-to-point transfers via RDMA, especially between various types of memory and NVIDIA GPUs. Backed by `LIBFABRIC <https://ofiwg.github.io/libfabric/>`__ on AWS instances with `EFA <https://aws.amazon.com/hpc/efa/>`__, and `UCX <https://github.com/openucx/ucx>`__ everywhere else.
For ease of following along, we'll start with the `Gloo <https://github.com/pytorch/gloo>`__ transport, which can be used without any physical GPUs.
.. _direct-transport-gloo:
Usage with Gloo (CPUs only)
---------------------------
Installation
^^^^^^^^^^^^
.. note::
Under construction.
Walkthrough
^^^^^^^^^^^
To get started, define an actor class and a task that returns a ``torch.Tensor``:
.. literalinclude:: ../doc_code/direct_transport_gloo.py
:language: python
:start-after: __normal_example_start__
:end-before: __normal_example_end__
As written, when the ``torch.Tensor`` is returned, it will be copied into Ray's CPU-based object store.
For CPU-based tensors, this can require an expensive step to copy and serialize the object, while GPU-based tensors additionally require a copy to and from CPU memory.
To enable RDT, use the ``tensor_transport`` option in the :func:`@ray.method <ray.method>` decorator.
.. literalinclude:: ../doc_code/direct_transport_gloo.py
:language: python
:start-after: __gloo_example_start__
:end-before: __gloo_example_end__
This decorator can be added to any actor tasks that return a ``torch.Tensor``, or that return ``torch.Tensors`` nested inside other Python objects.
Adding this decorator will change Ray's behavior in the following ways:
1. When returning the tensor, Ray will store a *reference* to the tensor instead of copying it to CPU memory.
2. When the :class:`ray.ObjectRef` is passed to another task, Ray will use Gloo to transfer the tensor to the destination task.
Note that for (2) to work, the :func:`@ray.method(tensor_transport) <ray.method>` decorator only needs to be added to the actor task that *returns* the tensor. It should not be added to actor tasks that *consume* the tensor (unless those tasks also return tensors).
Also, for (2) to work, we must first create a *collective group* of actors.
Creating a collective group
^^^^^^^^^^^^^^^^^^^^^^^^^^^
To create a collective group for use with RDT:
1. Create multiple Ray actors.
2. Create a collective group on the actors using the :func:`ray.experimental.collective.create_collective_group <ray.experimental.collective.create_collective_group>` function. The `backend` specified must match the `tensor_transport` used in the :func:`@ray.method <ray.method>` decorator.
Here is an example:
.. literalinclude:: ../doc_code/direct_transport_gloo.py
:language: python
:start-after: __gloo_group_start__
:end-before: __gloo_group_end__
The actors can now communicate directly via gloo.
The group can also be destroyed using the :func:`ray.experimental.collective.destroy_collective_group <ray.experimental.collective.destroy_collective_group>` function.
After calling this function, a new collective group can be created on the same actors.
Passing objects to other actors
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Now that we have a collective group, we can create and pass RDT objects between the actors.
Here is a full example:
.. literalinclude:: ../doc_code/direct_transport_gloo.py
:language: python
:start-after: __gloo_full_example_start__
:end-before: __gloo_full_example_end__
When the :class:`ray.ObjectRef` is passed to another task, Ray will use Gloo to transfer the tensor directly from the source actor to the destination actor instead of the default object store.
Note that the :func:`@ray.method(tensor_transport) <ray.method>` decorator is only added to the actor task that *returns* the tensor; once this hint has been added, the receiving actor task `receiver.sum` will automatically use Gloo to receive the tensor.
In this example, because `MyActor.sum` does not have the :func:`@ray.method(tensor_transport) <ray.method>` decorator, it will use the default Ray object store transport to return `torch.sum(tensor)`.
RDT also supports passing tensors nested inside Python data structures, as well as actor tasks that return multiple tensors, like in this example:
.. literalinclude:: ../doc_code/direct_transport_gloo.py
:language: python
:start-after: __gloo_multiple_tensors_example_start__
:end-before: __gloo_multiple_tensors_example_end__
Passing RDT objects to the actor that produced them
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RDT :class:`ray.ObjectRefs <ray.ObjectRef>` can also be passed to the actor that produced them.
This avoids any copies and just provides a reference to the same ``torch.Tensor`` that was previously created.
For example:
.. literalinclude:: ../doc_code/direct_transport_gloo.py
:language: python
:start-after: __gloo_intra_actor_start__
:end-before: __gloo_intra_actor_end__
.. note::
Ray only keeps a reference to the tensor created by the user, so the tensor objects are *mutable*.
If ``sender.sum`` were to modify the tensor in the above example, the changes would also be seen by ``receiver.sum``.
This differs from the normal Ray Core API, which always makes an immutable copy of data returned by actors.
``ray.get``
^^^^^^^^^^^
The :func:`ray.get <ray.get>` function can also be used as usual to retrieve the result of an RDT object. However, :func:`ray.get <ray.get>` will by default use the same tensor transport as the one specified in the :func:`@ray.method <ray.method>` decorator. For collective-based transports, this will not work if the caller is not part of the collective group.
Therefore, users need to specify the Ray object store as the tensor transport explicitly by setting ``_use_object_store`` in :func:`ray.get <ray.get>`.
.. literalinclude:: ../doc_code/direct_transport_gloo.py
:language: python
:start-after: __gloo_get_start__
:end-before: __gloo_get_end__
Object mutability
^^^^^^^^^^^^^^^^^
Unlike objects in the Ray object store, RDT objects are *mutable*, meaning that Ray only holds a reference to the tensor and will not copy it until a transfer is requested.
This means that if the actor that returns a tensor also keeps a reference to the tensor, and the actor later modifies it in place while Ray is still storing the tensor reference, it's possible that some or all of the changes may be seen by receiving actors.
Here is an example of what can go wrong:
.. literalinclude:: ../doc_code/direct_transport_gloo.py
:language: python
:start-after: __gloo_wait_tensor_freed_bad_start__
:end-before: __gloo_wait_tensor_freed_bad_end__
In this example, the sender actor returns a tensor to Ray, but it also keeps a reference to the tensor in its local state.
Then, in `sender.increment_and_sum_stored_tensor`, the sender actor modifies the tensor in place while Ray is still holding the tensor reference.
Then, the `receiver.increment_and_sum` task receives the modified tensor instead of the original, so the assertion fails.
To fix this kind of error, use the :func:`ray.experimental.wait_tensor_freed <ray.experimental.wait_tensor_freed>` function to wait for Ray to release all references to the tensor, so that the actor can safely write to the tensor again.
:func:`wait_tensor_freed <ray.experimental.wait_tensor_freed>` will unblock once all tasks that depend on the tensor have finished executing and all corresponding `ObjectRefs` have gone out of scope.
Ray tracks tasks that depend on the tensor by keeping track of which tasks take the `ObjectRef` corresponding to the tensor as an argument.
Here's a fixed version of the earlier example.
.. literalinclude:: ../doc_code/direct_transport_gloo.py
:language: python
:start-after: __gloo_wait_tensor_freed_start__
:end-before: __gloo_wait_tensor_freed_end__
The main changes are:
1. `sender` calls :func:`wait_tensor_freed <ray.experimental.wait_tensor_freed>` before modifying the tensor in place.
2. The driver skips :func:`ray.get <ray.get>` because :func:`wait_tensor_freed <ray.experimental.wait_tensor_freed>` blocks until all `ObjectRefs` pointing to the tensor are freed, so calling :func:`ray.get <ray.get>` here would cause a deadlock.
3. The driver calls `del tensor` to release its reference to the tensor. Again, this is necessary because :func:`wait_tensor_freed <ray.experimental.wait_tensor_freed>` blocks until all `ObjectRefs` pointing to the tensor are freed.
When an RDT `ObjectRef` is passed back to the same actor that produced it, Ray passes back a *reference* to the tensor instead of a copy. Therefore, the same kind of bug can occur.
To help catch such cases, Ray will print a warning if an RDT object is passed to the actor that produced it and a different actor, like so:
.. literalinclude:: ../doc_code/direct_transport_gloo.py
:language: python
:start-after: __gloo_object_mutability_warning_start__
:end-before: __gloo_object_mutability_warning_end__
Usage with NCCL (NVIDIA GPUs only)
----------------------------------
RDT requires just a few lines of code change to switch tensor transports. Here is the :ref:`Gloo example <direct-transport-gloo>`, modified to use NVIDIA GPUs and the `NCCL <https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/index.html>`__ library for collective GPU communication.
.. literalinclude:: ../doc_code/direct_transport_nccl.py
:language: python
:start-after: __nccl_full_example_start__
:end-before: __nccl_full_example_end__
The main code differences are:
1. The :func:`@ray.method <ray.method>` uses ``tensor_transport="nccl"`` instead of ``tensor_transport="gloo"``.
2. The :func:`ray.experimental.collective.create_collective_group <ray.experimental.collective.create_collective_group>` function is used to create a collective group.
3. The tensor is created on the GPU using the ``.cuda()`` method.
Usage with NIXL (CPUs or NVIDIA GPUs)
-------------------------------------
Installation
^^^^^^^^^^^^
First, install NIXL with a plain ``pip install nixl``.
For maximum performance, run the `install_gdrcopy.sh <https://github.com/ray-project/ray/blob/master/doc/tools/install_gdrcopy.sh>`__ script (e.g., ``install_gdrcopy.sh "${GDRCOPY_OS_VERSION}" "12.8" "x64"``). You can find available OS versions `here <https://developer.download.nvidia.com/compute/redist/gdrcopy/CUDA%2012.8/>`__.
When Ray selects the ``UCX`` backend (see :ref:`NIXL backend selection <nixl-backend-selection>` below), set these UCX environment variables to either let UCX choose the right transport from all options, or so that you can set your preferred transport option yourself. These variables apply only to the ``UCX`` backend and have no effect when Ray runs the ``LIBFABRIC`` backend.
.. code-block:: bash
# Example UCX configuration, adjust according to your environment
$ export UCX_TLS=all # or specify specific transports like "rc,ud,sm,^cuda_ipc" ..etc
$ export UCX_NET_DEVICES=all # or specify network devices like "mlx5_0:1,mlx5_1:1"
On the ``LIBFABRIC`` backend, use libfabric environment variables instead, such as ``FI_PROVIDER=efa`` to pin the provider and ``FI_LOG_LEVEL=Debug`` for diagnostics. See the `NIXL libfabric plugin documentation <https://github.com/ai-dynamo/nixl/blob/main/src/plugins/libfabric/README.md>`__ for additional configuration and troubleshooting.
.. _nixl-backend-selection:
NIXL backend selection
^^^^^^^^^^^^^^^^^^^^^^
Ray automatically selects the NIXL transport backend based on the available hardware:
- **AWS instances with EFA**: Ray detects EFA devices and validates that a realistic-size CUDA memory registration succeeds before it selects the ``LIBFABRIC`` backend. If validation fails, GPUDirect is usually misconfigured (for example, missing ``nvidia-peermem`` or dmabuf support). To detect EFA both on bare hosts and inside containers, Ray checks for the EFA network device (``/sys/class/net/efa*``) and for rdma-verbs devices bound to the kernel ``efa`` driver (under ``/sys/class/infiniband``). The host network device isn't visible inside a pod, so the verbs check is what makes detection work under Kubernetes. Ordinary InfiniBand or RoCE NICs also expose verbs devices, so Ray confirms the ``efa`` driver binding to avoid treating them as EFA.
- **All other environments**: Ray uses the ``UCX`` backend.
No configuration is required. On AWS EFA instances, make sure the `EFA installer <https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/efa-start.html>`__ has been run, which installs both the EFA driver and libfabric. If LIBFABRIC validation fails at startup, see the `NIXL libfabric plugin documentation <https://github.com/ai-dynamo/nixl/blob/main/src/plugins/libfabric/README.md>`__ for troubleshooting.
Walkthrough
^^^^^^^^^^^
NIXL can transfer data between different devices, including CPUs and NVIDIA GPUs, but doesn't require a collective group to be created ahead of time.
This means that any actor that has NIXL installed in its environment can be used to create and pass an RDT object.
Otherwise, the usage is the same as in the :ref:`Gloo example <direct-transport-gloo>`.
Here is an example showing how to use NIXL to transfer an RDT object between two actors:
.. literalinclude:: ../doc_code/direct_transport_nixl.py
:language: python
:start-after: __nixl_full_example_start__
:end-before: __nixl_full_example_end__
Compared to the :ref:`Gloo example <direct-transport-gloo>`, the main code differences are:
1. The :func:`@ray.method <ray.method>` uses ``tensor_transport="nixl"`` instead of ``tensor_transport="gloo"``.
2. No collective group is needed.
ray.put and ray.get with NIXL
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Unlike the collective-based tensor transports (Gloo and NCCL), the :func:`ray.get <ray.get>` function can use NIXL to retrieve a copy of the result.
By default, the tensor transport for :func:`ray.get <ray.get>` will be the one specified in the :func:`@ray.method <ray.method>` decorator.
.. literalinclude:: ../doc_code/direct_transport_nixl.py
:language: python
:start-after: __nixl_get_start__
:end-before: __nixl_get_end__
You can also use NIXL to retrieve the result from references created by :func:`ray.put <ray.put>`.
.. literalinclude:: ../doc_code/direct_transport_nixl.py
:language: python
:start-after: __nixl_put__and_get_start__
:end-before: __nixl_put__and_get_end__
Summary
-------
RDT allows Ray to store and pass objects directly between Ray actors, using accelerated transports like GLOO, NCCL, and NIXL.
Here are the main points to keep in mind:
* If using a collective-based tensor transport (Gloo or NCCL), a collective group must be created ahead of time. NIXL just requires all involved actors to have NIXL installed.
* Unlike objects in the Ray object store, RDT objects are *mutable*, meaning that Ray only holds a reference, not a copy, to the stored tensor(s).
* Otherwise, actors can be used as normal.
For a full list of limitations, see the :ref:`limitations <limitations>` section.
Microbenchmarks
===============
.. note::
Under construction.
.. _limitations:
Limitations
===========
RDT is currently in alpha and currently has the following limitations, which may be addressed in future releases:
* Support for ``torch.Tensor`` objects only.
* Support for Ray actors only, not Ray tasks.
* Support for the following transports: GLOO, NCCL, and NIXL.
* Support for CPUs and NVIDIA GPUs only.
* RDT objects are *mutable*. This means that Ray only holds a reference to the tensor, and will not copy it until a transfer is requested. Thus, if the application code also keeps a reference to a tensor before returning it, and modifies the tensor in place, then some or all of the changes may be seen by the receiving actor.
* `await` on an RDT ref is temporarily not supported.
For collective-based / two-sided tensor transports (Gloo and NCCL):
* Only the process that created the collective group can submit actor tasks that return and pass RDT objects. If the creating process passes the actor handles to other processes, those processes can submit actor tasks as usual, but will not be able to use RDT objects.
* Similarly, the process that created the collective group cannot serialize and pass RDT :class:`ray.ObjectRefs <ray.ObjectRef>` to other Ray tasks or actors. Instead, the :class:`ray.ObjectRef`\s can only be passed as direct arguments to other actor tasks, and those actors must be in the same collective group.
* Each actor can only be in one collective group per tensor transport at a time.
* No support for :func:`ray.put <ray.put>`.
* No support for out-of-order actors such as async actors or actors with ``max_concurrency`` > 1.
Due to a known issue, for NIXL, we currently do not support storing different GPU objects at the same actor, where the objects contain an overlapping but not equal set of tensors. To support this pattern, ensure that the first `ObjectRef` has gone out of scope before storing the same tensor(s) again in a second object.
.. literalinclude:: ../doc_code/direct_transport_nixl.py
:language: python
:start-after: __nixl_limitations_start__
:end-before: __nixl_limitations_end__
Error handling
==============
* Application-level errors, i.e. exceptions raised by user code, will not destroy the collective group and will instead be propagated to any dependent task(s), as for non-RDT Ray objects.
* If a system-level error occurs during a GLOO or NCCL collective operation, the collective group will be destroyed and the actors will be killed to prevent any hanging.
* If a system-level error occurs during a NIXL transfer, Ray or NIXL will abort the transfer with an exception and Ray will raise the exception in the dependent task or on the ray.get on the NIXL ref.
* System-level errors include:
* Errors internal to the third-party transport, e.g., NCCL network errors
* Actor or node failures
* Transport errors due to tensor device / transport mismatches, e.g., a CPU tensor when using NCCL
* Ray RDT object fetch timeouts (can be overridden by setting the ``RAY_rdt_fetch_fail_timeout_milliseconds`` environment variable)
* Any unexpected system bugs
Advanced: Registering a custom tensor transport
===============================================
Ray allows you to register custom tensor transports at runtime for use with RDT.
To implement a custom tensor transport, you can implement the abstract interface :class:`ray.experimental.TensorTransportManager <ray.experimental.TensorTransportManager>` and register it using :func:`ray.experimental.register_tensor_transport <ray.experimental.register_tensor_transport>`.
For a complete guide on implementing custom tensor transports, including detailed documentation of all required methods, see :ref:`custom-tensor-transport`.
Advanced: RDT Internals
=======================
.. note::
Under construction.
Table of Contents
-----------------
Learn more details about Ray Direct Transport from the following links.
.. toctree::
:maxdepth: 1
custom-tensor-transport
@@ -0,0 +1,32 @@
import ray
import asyncio
import requests
from aiohttp import web
@ray.remote
class Counter:
async def __init__(self):
self.counter = 0
asyncio.get_running_loop().create_task(self.run_http_server())
async def run_http_server(self):
app = web.Application()
app.add_routes([web.get("/", self.get)])
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "127.0.0.1", 25001)
await site.start()
async def get(self, request):
return web.Response(text=str(self.counter))
async def increment(self):
self.counter = self.counter + 1
ray.init()
counter = Counter.remote()
[ray.get(counter.increment.remote()) for i in range(5)]
r = requests.get("http://127.0.0.1:25001/")
assert r.text == "5"
@@ -0,0 +1,17 @@
import ray
from ray.util import ActorPool
@ray.remote
class Actor:
def double(self, n):
return n * 2
a1, a2 = Actor.remote(), Actor.remote()
pool = ActorPool([a1, a2])
# pool.map(..) returns a Python generator object ActorPool.map
gen = pool.map(lambda a, v: a.double.remote(v), [1, 2, 3, 4])
print(list(gen))
# [2, 4, 6, 8]
@@ -0,0 +1,23 @@
import ray
from ray.util.queue import Queue, Empty
ray.init()
# You can pass this object around to different tasks/actors
queue = Queue(maxsize=100)
@ray.remote
def consumer(id, queue):
try:
while True:
next_item = queue.get(block=True, timeout=1)
print(f"consumer {id} got work {next_item}")
except Empty:
pass
[queue.put(i) for i in range(10)]
print("Put work 1 - 10 to queue...")
consumers = [consumer.remote(id, queue) for id in range(2)]
ray.get(consumers)
@@ -0,0 +1,19 @@
import ray
@ray.remote
class MyActor:
def __init__(self, index):
self.index = index
def foo(self):
print("hello there")
def __repr__(self):
return f"MyActor(index={self.index})"
a = MyActor.remote(1)
b = MyActor.remote(2)
ray.get(a.foo.remote())
ray.get(b.foo.remote())
@@ -0,0 +1,46 @@
import asyncio
import ray
# We set num_cpus to zero because this actor will mostly just block on I/O.
@ray.remote(num_cpus=0)
class SignalActor:
def __init__(self):
self.ready_event = asyncio.Event()
def send(self, clear=False):
self.ready_event.set()
if clear:
self.ready_event.clear()
async def wait(self, should_wait=True):
if should_wait:
await self.ready_event.wait()
@ray.remote
def wait_and_go(signal):
ray.get(signal.wait.remote())
print("go!")
signal = SignalActor.remote()
tasks = [wait_and_go.remote(signal) for _ in range(4)]
print("ready...")
# Tasks will all be waiting for the signals.
print("set..")
ray.get(signal.send.remote())
# Tasks are unblocked.
ray.get(tasks)
# Output is:
# ready...
# set..
# (wait_and_go pid=77366) go!
# (wait_and_go pid=77372) go!
# (wait_and_go pid=77367) go!
# (wait_and_go pid=77358) go!
@@ -0,0 +1,95 @@
# __actor_checkpointing_manual_restart_begin__
import os
import sys
import ray
import json
import tempfile
import shutil
@ray.remote(num_cpus=1)
class Worker:
def __init__(self):
self.state = {"num_tasks_executed": 0}
def execute_task(self, crash=False):
if crash:
sys.exit(1)
# Execute the task
# ...
# Update the internal state
self.state["num_tasks_executed"] = self.state["num_tasks_executed"] + 1
def checkpoint(self):
return self.state
def restore(self, state):
self.state = state
class Controller:
def __init__(self):
self.worker = Worker.remote()
self.worker_state = ray.get(self.worker.checkpoint.remote())
def execute_task_with_fault_tolerance(self):
i = 0
while True:
i = i + 1
try:
ray.get(self.worker.execute_task.remote(crash=(i % 2 == 1)))
# Checkpoint the latest worker state
self.worker_state = ray.get(self.worker.checkpoint.remote())
return
except ray.exceptions.RayActorError:
print("Actor crashes, restarting...")
# Restart the actor and restore the state
self.worker = Worker.remote()
ray.get(self.worker.restore.remote(self.worker_state))
controller = Controller()
controller.execute_task_with_fault_tolerance()
controller.execute_task_with_fault_tolerance()
assert ray.get(controller.worker.checkpoint.remote())["num_tasks_executed"] == 2
# __actor_checkpointing_manual_restart_end__
# __actor_checkpointing_auto_restart_begin__
@ray.remote(max_restarts=-1, max_task_retries=-1)
class ImmortalActor:
def __init__(self, checkpoint_file):
self.checkpoint_file = checkpoint_file
if os.path.exists(self.checkpoint_file):
# Restore from a checkpoint
with open(self.checkpoint_file, "r") as f:
self.state = json.load(f)
else:
self.state = {}
def update(self, key, value):
import random
if random.randrange(10) < 5:
sys.exit(1)
self.state[key] = value
# Checkpoint the latest state
with open(self.checkpoint_file, "w") as f:
json.dump(self.state, f)
def get(self, key):
return self.state[key]
checkpoint_dir = tempfile.mkdtemp()
actor = ImmortalActor.remote(os.path.join(checkpoint_dir, "checkpoint.json"))
ray.get(actor.update.remote("1", 1))
ray.get(actor.update.remote("2", 2))
assert ray.get(actor.get.remote("1")) == 1
shutil.rmtree(checkpoint_dir)
# __actor_checkpointing_auto_restart_end__
@@ -0,0 +1,44 @@
# flake8: noqa
# fmt: off
# __actor_creator_failure_begin__
import ray
import os
import signal
ray.init()
@ray.remote(max_restarts=-1)
class Actor:
def ping(self):
return "hello"
@ray.remote
class Parent:
def generate_actors(self):
self.child = Actor.remote()
self.detached_actor = Actor.options(name="actor", lifetime="detached").remote()
return self.child, self.detached_actor, os.getpid()
parent = Parent.remote()
actor, detached_actor, pid = ray.get(parent.generate_actors.remote())
os.kill(pid, signal.SIGKILL)
try:
print("actor.ping:", ray.get(actor.ping.remote()))
except ray.exceptions.RayActorError as e:
print("Failed to submit actor call", e)
# Failed to submit actor call The actor died unexpectedly before finishing this task.
# class_name: Actor
# actor_id: 56f541b178ff78470f79c3b601000000
# namespace: ea8b3596-7426-4aa8-98cc-9f77161c4d5f
# The actor is dead because because all references to the actor were removed.
try:
print("detached_actor.ping:", ray.get(detached_actor.ping.remote()))
except ray.exceptions.RayActorError as e:
print("Failed to submit detached actor call", e)
# detached_actor.ping: hello
# __actor_creator_failure_end__
# fmt: on
@@ -0,0 +1,44 @@
# flake8: noqa
# fmt: off
# __actor_restart_begin__
import os
import ray
ray.init()
# This actor kills itself after executing 10 tasks.
@ray.remote(max_restarts=4, max_task_retries=-1)
class Actor:
def __init__(self):
self.counter = 0
def increment_and_possibly_fail(self):
# Exit after every 10 tasks.
if self.counter == 10:
os._exit(0)
self.counter += 1
return self.counter
actor = Actor.remote()
# The actor will be reconstructed up to 4 times, so we can execute up to 50
# tasks successfully. The actor is reconstructed by rerunning its constructor.
# Methods that were executing when the actor died will be retried and will not
# raise a `RayActorError`. Retried methods may execute twice, once on the
# failed actor and a second time on the restarted actor.
for _ in range(50):
counter = ray.get(actor.increment_and_possibly_fail.remote())
print(counter) # Prints the sequence 1-10 5 times.
# After the actor has been restarted 4 times, all subsequent methods will
# raise a `RayActorError`.
for _ in range(10):
try:
counter = ray.get(actor.increment_and_possibly_fail.remote())
print(counter) # Unreachable.
except ray.exceptions.RayActorError:
print("FAILURE") # Prints 10 times.
# __actor_restart_end__
# fmt: on
+91
View File
@@ -0,0 +1,91 @@
# flake8: noqa
# __cancel_start__
import ray
import asyncio
import time
@ray.remote
class Actor:
async def f(self):
try:
await asyncio.sleep(5)
except asyncio.CancelledError:
print("Actor task canceled.")
actor = Actor.remote()
ref = actor.f.remote()
# Wait until task is scheduled.
time.sleep(1)
ray.cancel(ref)
try:
ray.get(ref)
except ray.exceptions.RayTaskError:
print("Object reference was cancelled.")
# __cancel_end__
# __cancel_graceful_actor_start__
import ray
import time
@ray.remote
class SyncActor:
def __init__(self):
self.is_canceled = False
def long_running_method(self):
"""A sync actor method that checks for cancellation periodically."""
for i in range(100):
# For sync actor tasks, is_canceled() can be checked in the task body
if ray.get_runtime_context().is_canceled():
self.is_canceled = True
print("Actor task canceled, cleaning up...")
return "canceled"
time.sleep(0.1)
return "completed"
def get_cancel_status(self):
return self.is_canceled
# Sync actor task cancellation with periodic checking
actor = SyncActor.remote()
actor_task_ref = actor.long_running_method.remote()
# Wait until task is scheduled.
time.sleep(1)
ray.cancel(actor_task_ref)
# The TaskCancelledError will be raised when calling ray.get
try:
result = ray.get(actor_task_ref)
except ray.exceptions.TaskCancelledError:
print("Actor task was cancelled")
# The get_cancel_status will return True after cancellation
cancel_status = ray.get(actor.get_cancel_status.remote())
print(f"Actor detected cancellation: {cancel_status}")
# __cancel_graceful_actor_end__
# __enable_task_events_start__
@ray.remote
class FooActor:
# Disable task events reporting for this method.
@ray.method(enable_task_events=False)
def foo(self):
pass
foo_actor = FooActor.remote()
ray.get(foo_actor.foo.remote())
# __enable_task_events_end__
@@ -0,0 +1,44 @@
# __anti_pattern_start__
import ray
import numpy as np
ray.init()
large_object = np.zeros(10 * 1024 * 1024)
@ray.remote
def f1():
return len(large_object) # large_object is serialized along with f1!
ray.get(f1.remote())
# __anti_pattern_end__
# __better_approach_1_start__
large_object_ref = ray.put(np.zeros(10 * 1024 * 1024))
@ray.remote
def f2(large_object):
return len(large_object)
# Large object is passed through object store.
ray.get(f2.remote(large_object_ref))
# __better_approach_1_end__
# __better_approach_2_start__
large_object_creator = lambda: np.zeros(10 * 1024 * 1024) # noqa E731
@ray.remote
def f3():
large_object = (
large_object_creator()
) # Lambda is small compared with the large object.
return len(large_object)
ray.get(f3.remote())
# __better_approach_2_end__
@@ -0,0 +1,44 @@
import os
os.environ["RAY_DEDUP_LOGS"] = "0"
import ray
from concurrent.futures import ProcessPoolExecutor, as_completed
import multiprocessing
import numpy as np
@ray.remote
def generate_response(request):
print(request)
array = np.ones(100000)
return array
def process_response(response, idx):
print(f"Processing response {idx}")
return response
def main():
ray.init()
responses = ray.get([generate_response.remote(f"request {i}") for i in range(4)])
# Better approach: Set the start method to "spawn"
multiprocessing.set_start_method("spawn", force=True)
with ProcessPoolExecutor(max_workers=4) as executor:
future_to_task = {}
for idx, response in enumerate(responses):
future_to_task[executor.submit(process_response, response, idx)] = idx
for future in as_completed(future_to_task):
idx = future_to_task[future]
response_entry = future.result()
print(f"Response {idx} processed: {response_entry}")
ray.shutdown()
if __name__ == "__main__":
main()
@@ -0,0 +1,51 @@
# __anti_pattern_start__
import ray
ray.init()
global_var = 3
@ray.remote
class Actor:
def f(self):
return global_var + 3
actor = Actor.remote()
global_var = 4
# This returns 6, not 7. It is because the value change of global_var
# inside a driver is not reflected to the actor
# because they are running in different processes.
assert ray.get(actor.f.remote()) == 6
# __anti_pattern_end__
# __better_approach_start__
@ray.remote
class GlobalVarActor:
def __init__(self):
self.global_var = 3
def set_global_var(self, var):
self.global_var = var
def get_global_var(self):
return self.global_var
@ray.remote
class Actor:
def __init__(self, global_var_actor):
self.global_var_actor = global_var_actor
def f(self):
return ray.get(self.global_var_actor.get_global_var.remote()) + 3
global_var_actor = GlobalVarActor.remote()
actor = Actor.remote(global_var_actor)
ray.get(global_var_actor.set_global_var.remote(4))
# This returns 7 correctly.
assert ray.get(actor.f.remote()) == 7
# __better_approach_end__
@@ -0,0 +1,27 @@
# __anti_pattern_start__
import ray
import time
@ray.remote
def f():
return 1
@ray.remote
def pass_via_nested_ref(refs):
print(sum(ray.get(refs)))
@ray.remote
def pass_via_direct_arg(*args):
print(sum(args))
# Anti-pattern: Passing nested refs requires `ray.get` in a nested task.
ray.get(pass_via_nested_ref.remote([f.remote() for _ in range(3)]))
# Better approach: Pass refs as direct arguments. Use *args syntax to unpack
# multiple arguments.
ray.get(pass_via_direct_arg.remote(*[f.remote() for _ in range(3)]))
# __anti_pattern_end__
@@ -0,0 +1,64 @@
# __anti_pattern_start__
import ray
import pickle
from ray._private.internal_api import memory_summary
import ray.exceptions
ray.init()
@ray.remote
def out_of_band_serialization_pickle():
obj_ref = ray.put(1)
import pickle
# object_ref is serialized from user code using a regular pickle.
# Ray can't keep track of the reference, so the underlying object
# can be GC'ed unexpectedly, which can cause unexpected hangs.
return pickle.dumps(obj_ref)
@ray.remote
def out_of_band_serialization_ray_cloudpickle():
obj_ref = ray.put(1)
from ray import cloudpickle
# ray.cloudpickle can serialize only when
# RAY_allow_out_of_band_object_ref_serialization=1 env var is set.
# However, the object_ref is pinned for the lifetime of the worker,
# which can cause Ray object leaks that can cause spilling.
return cloudpickle.dumps(obj_ref)
print("==== serialize object ref with pickle ====")
result = ray.get(out_of_band_serialization_pickle.remote())
try:
ray.get(pickle.loads(result), timeout=5)
except ray.exceptions.GetTimeoutError:
print("Underlying object is unexpectedly GC'ed!\n\n")
print("==== serialize object ref with ray.cloudpickle ====")
# By default, it's allowed to serialize ray.ObjectRef using
# ray.cloudpickle.
ray.get(out_of_band_serialization_ray_cloudpickle.options().remote())
# you can see objects are still pinned although it's GC'ed and not used anymore.
print(memory_summary())
print(
"==== serialize object ref with ray.cloudpickle with env var "
"RAY_allow_out_of_band_object_ref_serialization=0 for debugging ===="
)
try:
ray.get(
out_of_band_serialization_ray_cloudpickle.options(
runtime_env={
"env_vars": {
"RAY_allow_out_of_band_object_ref_serialization": "0",
}
}
).remote()
)
except Exception as e:
print(f"Exception raised from out_of_band_serialization_ray_cloudpickle {e}\n\n")
# __anti_pattern_end__
@@ -0,0 +1,23 @@
# __anti_pattern_start__
import ray
import numpy as np
ray.init()
@ray.remote
def func(large_arg, i):
return len(large_arg) + i
large_arg = np.zeros(1024 * 1024)
# 10 copies of large_arg are stored in the object store.
outputs = ray.get([func.remote(large_arg, i) for i in range(10)])
# __anti_pattern_end__
# __better_approach_start__
# 1 copy of large_arg is stored in the object store.
large_arg_ref = ray.put(large_arg)
outputs = ray.get([func.remote(large_arg_ref, i) for i in range(10)])
# __better_approach_end__
@@ -0,0 +1,25 @@
# __anti_pattern_start__
import ray
ray.init()
@ray.remote
def f(i):
return i
# Anti-pattern: no parallelism due to calling ray.get inside of the loop.
sequential_returns = []
for i in range(100):
sequential_returns.append(ray.get(f.remote(i)))
# Better approach: parallelism because the tasks are executed in parallel.
refs = []
for i in range(100):
refs.append(f.remote(i))
parallel_returns = ray.get(refs)
# __anti_pattern_end__
assert sequential_returns == parallel_returns
@@ -0,0 +1,36 @@
# __anti_pattern_start__
import random
import time
import ray
ray.init()
@ray.remote
def f(i):
time.sleep(random.random())
return i
# Anti-pattern: process results in the submission order.
sum_in_submission_order = 0
refs = [f.remote(i) for i in range(100)]
for ref in refs:
# Blocks until this ObjectRef is ready.
result = ray.get(ref)
# process result
sum_in_submission_order = sum_in_submission_order + result
# Better approach: process results in the completion order.
sum_in_completion_order = 0
refs = [f.remote(i) for i in range(100)]
unfinished = refs
while unfinished:
# Returns the first ObjectRef that is ready.
finished, unfinished = ray.wait(unfinished, num_returns=1)
result = ray.get(finished[0])
# process result
sum_in_completion_order = sum_in_completion_order + result
# __anti_pattern_end__
assert sum_in_submission_order == sum_in_completion_order
@@ -0,0 +1,37 @@
# __anti_pattern_start__
import ray
import numpy as np
ray.init()
def process_results(results):
# custom process logic
pass
@ray.remote
def return_big_object():
return np.zeros(1024 * 10)
NUM_TASKS = 1000
object_refs = [return_big_object.remote() for _ in range(NUM_TASKS)]
# This will fail with heap out-of-memory
# or object store out-of-space if NUM_TASKS is large enough.
results = ray.get(object_refs)
process_results(results)
# __anti_pattern_end__
# __better_approach_start__
BATCH_SIZE = 100
while object_refs:
# Process results in the finish order instead of the submission order.
ready_object_refs, object_refs = ray.wait(object_refs, num_returns=BATCH_SIZE)
# The node only needs enough space to store
# a batch of objects instead of all objects.
results = ray.get(ready_object_refs)
process_results(results)
# __better_approach_end__
@@ -0,0 +1,34 @@
# __anti_pattern_start__
import ray
ray.init()
outputs = []
for i in range(10):
@ray.remote
def double(i):
return i * 2
outputs.append(double.remote(i))
outputs = ray.get(outputs)
# The double remote function is pickled and uploaded 10 times.
# __anti_pattern_end__
assert outputs == [i * 2 for i in range(10)]
# __better_approach_start__
@ray.remote
def double(i):
return i * 2
outputs = []
for i in range(10):
outputs.append(double.remote(i))
outputs = ray.get(outputs)
# The double remote function is pickled and uploaded 1 time.
# __better_approach_end__
assert outputs == [i * 2 for i in range(10)]
@@ -0,0 +1,151 @@
# __return_single_value_start__
import ray
import numpy as np
@ray.remote
def task_with_single_small_return_value_bad():
small_return_value = 1
# The value will be stored in the object store
# and the reference is returned to the caller.
small_return_value_ref = ray.put(small_return_value)
return small_return_value_ref
@ray.remote
def task_with_single_small_return_value_good():
small_return_value = 1
# Ray will return the value inline to the caller
# which is faster than the previous approach.
return small_return_value
assert ray.get(ray.get(task_with_single_small_return_value_bad.remote())) == ray.get(
task_with_single_small_return_value_good.remote()
)
@ray.remote
def task_with_single_large_return_value_bad():
large_return_value = np.zeros(10 * 1024 * 1024)
large_return_value_ref = ray.put(large_return_value)
return large_return_value_ref
@ray.remote
def task_with_single_large_return_value_good():
# Both approaches will store the large array to the object store
# but this is better since it's faster and more fault tolerant.
large_return_value = np.zeros(10 * 1024 * 1024)
return large_return_value
assert np.array_equal(
ray.get(ray.get(task_with_single_large_return_value_bad.remote())),
ray.get(task_with_single_large_return_value_good.remote()),
)
# Same thing applies for actor tasks as well.
@ray.remote
class Actor:
def task_with_single_return_value_bad(self):
single_return_value = np.zeros(9 * 1024 * 1024)
return ray.put(single_return_value)
def task_with_single_return_value_good(self):
return np.zeros(9 * 1024 * 1024)
actor = Actor.remote()
assert np.array_equal(
ray.get(ray.get(actor.task_with_single_return_value_bad.remote())),
ray.get(actor.task_with_single_return_value_good.remote()),
)
# __return_single_value_end__
# __return_static_multi_values_start__
# This will return a single object
# which is a tuple of two ObjectRefs to the actual values.
@ray.remote(num_returns=1)
def task_with_static_multiple_returns_bad1():
return_value_1_ref = ray.put(1)
return_value_2_ref = ray.put(2)
return (return_value_1_ref, return_value_2_ref)
# This will return two objects each of which is an ObjectRef to the actual value.
@ray.remote(num_returns=2)
def task_with_static_multiple_returns_bad2():
return_value_1_ref = ray.put(1)
return_value_2_ref = ray.put(2)
return (return_value_1_ref, return_value_2_ref)
# This will return two objects each of which is the actual value.
@ray.remote(num_returns=2)
def task_with_static_multiple_returns_good():
return_value_1 = 1
return_value_2 = 2
return (return_value_1, return_value_2)
assert (
ray.get(ray.get(task_with_static_multiple_returns_bad1.remote())[0])
== ray.get(ray.get(task_with_static_multiple_returns_bad2.remote()[0]))
== ray.get(task_with_static_multiple_returns_good.remote()[0])
)
@ray.remote
class Actor:
@ray.method(num_returns=1)
def task_with_static_multiple_returns_bad1(self):
return_value_1_ref = ray.put(1)
return_value_2_ref = ray.put(2)
return (return_value_1_ref, return_value_2_ref)
@ray.method(num_returns=2)
def task_with_static_multiple_returns_bad2(self):
return_value_1_ref = ray.put(1)
return_value_2_ref = ray.put(2)
return (return_value_1_ref, return_value_2_ref)
@ray.method(num_returns=2)
def task_with_static_multiple_returns_good(self):
# This is faster and more fault tolerant.
return_value_1 = 1
return_value_2 = 2
return (return_value_1, return_value_2)
actor = Actor.remote()
assert (
ray.get(ray.get(actor.task_with_static_multiple_returns_bad1.remote())[0])
== ray.get(ray.get(actor.task_with_static_multiple_returns_bad2.remote()[0]))
== ray.get(actor.task_with_static_multiple_returns_good.remote()[0])
)
# __return_static_multi_values_end__
# __return_dynamic_multi_values_start__
@ray.remote(num_returns=1)
def task_with_dynamic_returns_bad(n):
return_value_refs = []
for i in range(n):
return_value_refs.append(ray.put(np.zeros(i * 1024 * 1024)))
return return_value_refs
@ray.remote(num_returns="dynamic")
def task_with_dynamic_returns_good(n):
for i in range(n):
yield np.zeros(i * 1024 * 1024)
assert np.array_equal(
ray.get(ray.get(task_with_dynamic_returns_bad.remote(2))[0]),
ray.get(next(iter(ray.get(task_with_dynamic_returns_good.remote(2))))),
)
# __return_dynamic_multi_values_end__
@@ -0,0 +1,59 @@
# __anti_pattern_start__
import ray
import time
import itertools
ray.init()
numbers = list(range(10000))
def double(number):
time.sleep(0.00001)
return number * 2
start_time = time.time()
serial_doubled_numbers = [double(number) for number in numbers]
end_time = time.time()
print(f"Ordinary function call takes {end_time - start_time} seconds")
# Ordinary function call takes 0.16506004333496094 seconds
@ray.remote
def remote_double(number):
return double(number)
start_time = time.time()
doubled_number_refs = [remote_double.remote(number) for number in numbers]
parallel_doubled_numbers = ray.get(doubled_number_refs)
end_time = time.time()
print(f"Parallelizing tasks takes {end_time - start_time} seconds")
# Parallelizing tasks takes 1.6061789989471436 seconds
# __anti_pattern_end__
assert serial_doubled_numbers == parallel_doubled_numbers
# __batching_start__
@ray.remote
def remote_double_batch(numbers):
return [double(number) for number in numbers]
BATCH_SIZE = 1000
start_time = time.time()
doubled_batch_refs = []
for i in range(0, len(numbers), BATCH_SIZE):
batch = numbers[i : i + BATCH_SIZE]
doubled_batch_refs.append(remote_double_batch.remote(batch))
parallel_doubled_numbers_with_batching = list(
itertools.chain(*ray.get(doubled_batch_refs))
)
end_time = time.time()
print(f"Parallelizing tasks with batching takes {end_time - start_time} seconds")
# Parallelizing tasks with batching takes 0.030150890350341797 seconds
# __batching_end__
assert serial_doubled_numbers == parallel_doubled_numbers_with_batching
@@ -0,0 +1,33 @@
# __anti_pattern_start__
import ray
import numpy as np
ray.init()
@ray.remote
def generate_rollout():
return np.ones((10000, 10000))
@ray.remote
def reduce(rollout):
return np.sum(rollout)
# `ray.get()` downloads the result here.
rollout = ray.get(generate_rollout.remote())
# Now we have to reupload `rollout`
reduced = ray.get(reduce.remote(rollout))
# __anti_pattern_end__
assert reduced == 100000000
# __better_approach_start__
# Don't need ray.get here.
rollout_obj_ref = generate_rollout.remote()
# Rollout object is passed by reference.
reduced = ray.get(reduce.remote(rollout_obj_ref))
# __better_approach_end__
assert reduced == 100000000
@@ -0,0 +1,75 @@
import os
# CI environment is slow, set the timeout to 60 seconds
os.environ["RAY_CGRAPH_get_timeout"] = "60"
os.environ["RAY_CGRAPH_submit_timeout"] = "60"
# __cgraph_cpu_to_gpu_actor_start__
import torch
import ray
import ray.dag
@ray.remote(num_gpus=1)
class GPUActor:
def process(self, tensor: torch.Tensor):
assert tensor.device.type == "cuda"
return tensor.shape
actor = GPUActor.remote()
# __cgraph_cpu_to_gpu_actor_end__
# __cgraph_cpu_to_gpu_start__
with ray.dag.InputNode() as inp:
inp = inp.with_tensor_transport(device="cuda")
dag = actor.process.bind(inp)
cdag = dag.experimental_compile()
print(ray.get(cdag.execute(torch.zeros(10))))
# __cgraph_cpu_to_gpu_end__
# __cgraph_nccl_setup_start__
import torch
import ray
import ray.dag
from ray.experimental.channel.torch_tensor_type import TorchTensorType
# Note that the following example requires at least 2 GPUs.
assert (
ray.available_resources().get("GPU") >= 2
), "At least 2 GPUs are required to run this example."
@ray.remote(num_gpus=1)
class GPUSender:
def send(self, shape):
return torch.zeros(shape, device="cuda")
@ray.remote(num_gpus=1)
class GPUReceiver:
def recv(self, tensor: torch.Tensor):
assert tensor.device.type == "cuda"
return tensor.shape
sender = GPUSender.remote()
receiver = GPUReceiver.remote()
# __cgraph_nccl_setup_end__
# __cgraph_nccl_exec_start__
with ray.dag.InputNode() as inp:
dag = sender.send.bind(inp)
# Add a type hint that the return value of `send` should use NCCL.
dag = dag.with_tensor_transport("nccl")
# NOTE: With ray<2.42, use `with_type_hint()` instead.
# dag = dag.with_type_hint(TorchTensorType(transport="nccl"))
dag = receiver.recv.bind(dag)
# Compile API prepares the NCCL communicator across all workers and schedule operations
# accordingly.
dag = dag.experimental_compile()
assert ray.get(dag.execute((10,))) == (10,)
# __cgraph_nccl_exec_end__
@@ -0,0 +1,70 @@
# __cgraph_overlap_start__
import ray
import time
import torch
from ray.dag import InputNode, MultiOutputNode
@ray.remote(num_cpus=0, num_gpus=1)
class TorchTensorWorker:
def send(self, shape, dtype, value: int, send_tensor=True):
if not send_tensor:
return 1
return torch.ones(shape, dtype=dtype, device="cuda") * value
def recv_and_matmul(self, two_d_tensor):
"""
Receive the tensor and do some expensive computation (matmul).
Args:
two_d_tensor: a 2D tensor that has the same size for its dimensions
"""
# Check that tensor got loaded to the correct device.
assert two_d_tensor.dim() == 2
assert two_d_tensor.size(0) == two_d_tensor.size(1)
torch.matmul(two_d_tensor, two_d_tensor)
return (two_d_tensor[0][0].item(), two_d_tensor.shape, two_d_tensor.dtype)
def test(overlap_gpu_communication):
num_senders = 3
senders = [TorchTensorWorker.remote() for _ in range(num_senders)]
receiver = TorchTensorWorker.remote()
shape = (10000, 10000)
dtype = torch.float16
with InputNode() as inp:
branches = [sender.send.bind(shape, dtype, inp) for sender in senders]
branches = [
branch.with_tensor_transport(
transport="nccl", _static_shape=True, _direct_return=True
)
# For a ray version before 2.42, use `with_type_hint()` instead.
# branch.with_type_hint(
# TorchTensorType(
# transport="nccl", _static_shape=True, _direct_return=True
# )
# )
for branch in branches
]
branches = [receiver.recv_and_matmul.bind(branch) for branch in branches]
dag = MultiOutputNode(branches)
compiled_dag = dag.experimental_compile(
_overlap_gpu_communication=overlap_gpu_communication
)
start = time.monotonic()
for i in range(5):
ref = compiled_dag.execute(i)
result = ray.get(ref)
assert result == [(i, shape, dtype)] * num_senders
duration = time.monotonic() - start
print(f"{overlap_gpu_communication=}, {duration=}")
compiled_dag.teardown(kill_actors=True)
for overlap_gpu_communication in [False, True]:
test(overlap_gpu_communication)
# __cgraph_overlap_end__
@@ -0,0 +1,63 @@
import os
import subprocess
from pathlib import Path
NSIGHT_FAKE_DIR = str(
Path(os.path.realpath(__file__)).parents[4]
/ "python"
/ "ray"
/ "tests"
/ "nsight_fake"
)
subprocess.check_call(
["pip", "install", NSIGHT_FAKE_DIR],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
# __profiling_setup_start__
import ray
import torch
from ray.dag import InputNode
@ray.remote(num_gpus=1, runtime_env={"nsight": "default"})
class RayActor:
def send(self, shape, dtype, value: int):
return torch.ones(shape, dtype=dtype, device="cuda") * value
def recv(self, tensor):
return (tensor[0].item(), tensor.shape, tensor.dtype)
sender = RayActor.remote()
receiver = RayActor.remote()
# __profiling_setup_end__
# __profiling_execution_start__
shape = (10,)
dtype = torch.float16
# Test normal execution.
with InputNode() as inp:
dag = sender.send.bind(inp.shape, inp.dtype, inp[0])
dag = dag.with_tensor_transport(transport="nccl")
dag = receiver.recv.bind(dag)
compiled_dag = dag.experimental_compile()
for i in range(3):
shape = (10 * (i + 1),)
ref = compiled_dag.execute(i, shape=shape, dtype=dtype)
assert ray.get(ref) == (i, shape, dtype)
# __profiling_execution_end__
subprocess.check_call(
["pip", "uninstall", "nsys", "--y"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
@@ -0,0 +1,174 @@
# __simple_actor_start__
import ray
@ray.remote
class SimpleActor:
def echo(self, msg):
return msg
# __simple_actor_end__
# __ray_core_usage_start__
import time
a = SimpleActor.remote()
# warmup
for _ in range(5):
msg_ref = a.echo.remote("hello")
ray.get(msg_ref)
start = time.perf_counter()
msg_ref = a.echo.remote("hello")
ray.get(msg_ref)
end = time.perf_counter()
print(f"Execution takes {(end - start) * 1000 * 1000} us")
# __ray_core_usage_end__
# __dag_usage_start__
import ray.dag
with ray.dag.InputNode() as inp:
# Note that it uses `bind` instead of `remote`.
# This returns a ray.dag.DAGNode, instead of the usual ray.ObjectRef.
dag = a.echo.bind(inp)
# warmup
for _ in range(5):
msg_ref = dag.execute("hello")
ray.get(msg_ref)
start = time.perf_counter()
# `dag.execute` runs the DAG and returns an ObjectRef. You can use `ray.get` API.
msg_ref = dag.execute("hello")
ray.get(msg_ref)
end = time.perf_counter()
print(f"Execution takes {(end - start) * 1000 * 1000} us")
# __dag_usage_end__
# __cgraph_usage_start__
dag = dag.experimental_compile()
# warmup
for _ in range(5):
msg_ref = dag.execute("hello")
ray.get(msg_ref)
start = time.perf_counter()
# `dag.execute` runs the DAG and returns CompiledDAGRef. Similar to
# ObjectRefs, you can use the ray.get API.
msg_ref = dag.execute("hello")
ray.get(msg_ref)
end = time.perf_counter()
print(f"Execution takes {(end - start) * 1000 * 1000} us")
# __cgraph_usage_end__
# __teardown_start__
dag.teardown()
# __teardown_end__
# __cgraph_bind_start__
a = SimpleActor.remote()
b = SimpleActor.remote()
with ray.dag.InputNode() as inp:
# Note that it uses `bind` instead of `remote`.
# This returns a ray.dag.DAGNode, instead of the usual ray.ObjectRef.
dag = a.echo.bind(inp)
dag = b.echo.bind(dag)
dag = dag.experimental_compile()
print(ray.get(dag.execute("hello")))
# __cgraph_bind_end__
dag.teardown()
# __cgraph_multi_output_start__
import ray.dag
a = SimpleActor.remote()
b = SimpleActor.remote()
with ray.dag.InputNode() as inp:
# Note that it uses `bind` instead of `remote`.
# This returns a ray.dag.DAGNode, instead of the usual ray.ObjectRef.
dag = ray.dag.MultiOutputNode([a.echo.bind(inp), b.echo.bind(inp)])
dag = dag.experimental_compile()
print(ray.get(dag.execute("hello")))
# __cgraph_multi_output_end__
dag.teardown()
# __cgraph_async_compile_start__
import ray
@ray.remote
class EchoActor:
def echo(self, msg):
return msg
actor = EchoActor.remote()
with ray.dag.InputNode() as inp:
dag = actor.echo.bind(inp)
cdag = dag.experimental_compile(enable_asyncio=True)
# __cgraph_async_compile_end__
# __cgraph_async_execute_start__
import asyncio
async def async_method(i):
fut = await cdag.execute_async(i)
result = await fut
assert result == i
loop = asyncio.get_event_loop()
loop.run_until_complete(async_method(42))
# __cgraph_async_execute_end__
cdag.teardown()
# __cgraph_actor_death_start__
from ray.dag import InputNode, MultiOutputNode
@ray.remote
class EchoActor:
def echo(self, msg):
return msg
actors = [EchoActor.remote() for _ in range(4)]
with InputNode() as inp:
outputs = [actor.echo.bind(inp) for actor in actors]
dag = MultiOutputNode(outputs)
compiled_dag = dag.experimental_compile()
# Kill one of the actors to simulate unexpected actor death.
ray.kill(actors[0])
ref = compiled_dag.execute(1)
live_actors = []
try:
ray.get(ref)
except ray.exceptions.ActorDiedError:
# At this point, the Compiled Graph is shutting down.
for actor in actors:
try:
# Check for live actors.
ray.get(actor.echo.remote("ping"))
live_actors.append(actor)
except ray.exceptions.RayActorError:
pass
# Optionally, use the live actors to create a new Compiled Graph.
assert live_actors == actors[1:]
# __cgraph_actor_death_end__
@@ -0,0 +1,43 @@
# __numpy_troubleshooting_start__
import ray
import numpy as np
from ray.dag import InputNode
@ray.remote
class NumPyActor:
def get_arr(self, _):
numpy_arr = np.ones((5, 1024))
return numpy_arr
actor = NumPyActor.remote()
with InputNode() as inp:
dag = actor.get_arr.bind(inp)
cgraph = dag.experimental_compile()
for _ in range(5):
ref = cgraph.execute(0)
result = ray.get(ref)
# Adding this explicit del would fix any issues
# del result
# __numpy_troubleshooting_end__
# __teardown_troubleshooting_start__
@ray.remote
class SendActor:
def send(self, x):
return x
actor = SendActor.remote()
with InputNode() as inp:
dag = actor.send.bind(inp)
cgraph = dag.experimental_compile()
# Not adding this explicit teardown before reusing `actor` could cause problems
# cgraph.teardown()
with InputNode() as inp:
dag = actor.send.bind(inp)
cgraph = dag.experimental_compile()
# __teardown_troubleshooting_end__
@@ -0,0 +1,31 @@
# __cgraph_visualize_start__
import ray
from ray.dag import InputNode, MultiOutputNode
@ray.remote
class Worker:
def inc(self, x):
return x + 1
def double(self, x):
return x * 2
def echo(self, x):
return x
sender1 = Worker.remote()
sender2 = Worker.remote()
receiver = Worker.remote()
with InputNode() as inp:
w1 = sender1.inc.bind(inp)
w1 = receiver.echo.bind(w1)
w2 = sender2.double.bind(inp)
w2 = receiver.echo.bind(w2)
dag = MultiOutputNode([w1, w2])
compiled_dag = dag.experimental_compile()
compiled_dag.visualize()
# __cgraph_visualize_end__
@@ -0,0 +1,102 @@
# flake8: noqa
# fmt: off
# __crosslang_init_start__
import ray
ray.init(job_config=ray.job_config.JobConfig(code_search_path=["/path/to/code"]))
# __crosslang_init_end__
# fmt: on
# fmt: off
# __crosslang_multidir_start__
import ray
ray.init(job_config=ray.job_config.JobConfig(code_search_path="/path/to/jars:/path/to/pys"))
# __crosslang_multidir_end__
# fmt: on
# fmt: off
# __python_call_java_start__
import ray
with ray.init(job_config=ray.job_config.JobConfig(code_search_path=["/path/to/code"])):
# Define a Java class.
counter_class = ray.cross_language.java_actor_class(
"io.ray.demo.Counter")
# Create a Java actor and call actor method.
counter = counter_class.remote()
obj_ref1 = counter.increment.remote()
assert ray.get(obj_ref1) == 1
obj_ref2 = counter.increment.remote()
assert ray.get(obj_ref2) == 2
# Define a Java function.
add_function = ray.cross_language.java_function(
"io.ray.demo.Math", "add")
# Call the Java remote function.
obj_ref3 = add_function.remote(1, 2)
assert ray.get(obj_ref3) == 3
# __python_call_java_end__
# fmt: on
# fmt: off
# __python_module_start__
# /path/to/the_dir/ray_demo.py
import ray
@ray.remote
class Counter(object):
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
return self.value
@ray.remote
def add(a, b):
return a + b
# __python_module_end__
# fmt: on
# fmt: off
# __serialization_start__
# ray_serialization.py
import ray
@ray.remote
def py_return_input(v):
return v
# __serialization_end__
# fmt: on
# fmt: off
# __raise_exception_start__
# ray_exception.py
import ray
@ray.remote
def raise_exception():
1 / 0
# __raise_exception_end__
# fmt: on
# fmt: off
# __raise_exception_demo_start__
# ray_exception_demo.py
import ray
with ray.init(job_config=ray.job_config.JobConfig(code_search_path=["/path/to/ray_exception"])):
obj_ref = ray.cross_language.java_function(
"io.ray.demo.MyRayClass",
"raiseExceptionFromPython").remote()
ray.get(obj_ref) # <-- raise exception from here.
# __raise_exception_demo_end__
# fmt: on
+18
View File
@@ -0,0 +1,18 @@
import ray
import numpy as np
@ray.remote
def f(arr):
# arr = arr.copy() # Adding a copy will fix the error.
arr[0] = 1
try:
ray.get(f.remote(np.zeros(100)))
except ray.exceptions.RayTaskError as e:
print(e)
# ray.exceptions.RayTaskError(ValueError): ray::f()
# File "test.py", line 6, in f
# arr[0] = 1
# ValueError: assignment destination is read-only
@@ -0,0 +1,169 @@
# flake8: noqa
# __custom_metadata_start__
import multiprocessing.shared_memory as shm
import pickle
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
import numpy
import ray
from ray.experimental import (
CommunicatorMetadata,
TensorTransportManager,
TensorTransportMetadata,
register_tensor_transport,
)
@dataclass
class ShmTransportMetadata(TensorTransportMetadata):
"""Custom metadata that stores the shared memory name and size."""
shm_name: Optional[str] = None
shm_size: Optional[int] = None
@dataclass
class ShmCommunicatorMetadata(CommunicatorMetadata):
"""No extra communicator metadata needed for shared memory."""
pass
# __custom_metadata_end__
# __custom_properties_start__
class SharedMemoryTransport(TensorTransportManager):
"""A one-sided tensor transport that transfers numpy arrays through shared memory."""
def __init__(self):
self.shared_memory_objects: Dict[str, shm.SharedMemory] = {}
def tensor_transport_backend(self) -> str:
return "shared_memory"
@staticmethod
def is_one_sided() -> bool:
return True
@staticmethod
def can_abort_transport() -> bool:
return False
def actor_has_tensor_transport(self, actor: "ray.actor.ActorHandle") -> bool:
return True
# __custom_properties_end__
# __custom_extract_start__
def extract_tensor_transport_metadata(
self,
obj_id: str,
rdt_object: List[numpy.ndarray],
) -> TensorTransportMetadata:
# Record shapes and dtypes.
tensor_meta = []
if rdt_object:
for tensor in rdt_object:
tensor_meta.append((tensor.shape, tensor.dtype))
# Serialize the tensors and store them in shared memory.
serialized_rdt_object = pickle.dumps(rdt_object)
size = len(serialized_rdt_object)
name = obj_id[:20]
shm_obj = shm.SharedMemory(name=name, create=True, size=size)
shm_obj.buf[:size] = serialized_rdt_object
self.shared_memory_objects[obj_id] = shm_obj
return ShmTransportMetadata(
tensor_meta=tensor_meta, tensor_device="cpu", shm_name=name, shm_size=size
)
# __custom_extract_end__
# __custom_communicator_start__
def get_communicator_metadata(
self,
src_actor: "ray.actor.ActorHandle",
dst_actor: "ray.actor.ActorHandle",
backend: Optional[str] = None,
) -> CommunicatorMetadata:
return ShmCommunicatorMetadata()
# __custom_communicator_end__
# __custom_send_recv_start__
def recv_multiple_tensors(
self,
obj_id: str,
tensor_transport_metadata: TensorTransportMetadata,
communicator_metadata: CommunicatorMetadata,
target_buffers: Optional[List[Any]] = None,
):
# Open the shared memory block and deserialize.
shm_name = tensor_transport_metadata.shm_name
size = tensor_transport_metadata.shm_size
shm_block = shm.SharedMemory(name=shm_name)
recv_tensors = pickle.loads(shm_block.buf[:size])
shm_block.close()
return recv_tensors
def send_multiple_tensors(
self,
tensors: List[numpy.ndarray],
tensor_transport_metadata: TensorTransportMetadata,
communicator_metadata: CommunicatorMetadata,
):
raise NotImplementedError("One-sided transport doesn't use send.")
# __custom_send_recv_end__
# __custom_cleanup_start__
def garbage_collect(
self,
obj_id: str,
tensor_transport_meta: TensorTransportMetadata,
tensors: List[numpy.ndarray],
):
self.shared_memory_objects[obj_id].close()
self.shared_memory_objects[obj_id].unlink()
del self.shared_memory_objects[obj_id]
def abort_transport(
self,
obj_id: str,
communicator_metadata: CommunicatorMetadata,
):
pass
# __custom_cleanup_end__
# __custom_usage_start__
register_tensor_transport(
"shared_memory", # Transport name
["cpu"], # Supported device types
SharedMemoryTransport, # TensorTransportManager class
numpy.ndarray, # Data type for this transport
)
@ray.remote
class MyActor:
@ray.method(tensor_transport="shared_memory")
def echo(self, data):
return data
def sum(self, data):
return data.sum().item()
actors = [MyActor.remote() for _ in range(2)]
ref = actors[0].echo.remote(numpy.array([1, 2, 3]))
result = actors[1].sum.remote(ref)
print(ray.get(result))
# 6
# __custom_usage_end__
@@ -0,0 +1,277 @@
# flake8: noqa
# __normal_example_start__
import torch
import ray
@ray.remote
class MyActor:
def random_tensor(self):
return torch.randn(1000, 1000)
# __normal_example_end__
# __gloo_example_start__
@ray.remote
class MyActor:
@ray.method(tensor_transport="gloo")
def random_tensor(self):
return torch.randn(1000, 1000)
# __gloo_example_end__
# __gloo_group_start__
import torch
import ray
from ray.experimental.collective import create_collective_group
@ray.remote
class MyActor:
@ray.method(tensor_transport="gloo")
def random_tensor(self):
return torch.randn(1000, 1000)
def sum(self, tensor: torch.Tensor):
return torch.sum(tensor)
sender, receiver = MyActor.remote(), MyActor.remote()
# The tensor_transport specified here must match the one used in the @ray.method
# decorator.
group = create_collective_group([sender, receiver], backend="torch_gloo")
# __gloo_group_end__
# __gloo_group_destroy_start__
from ray.experimental.collective import destroy_collective_group
destroy_collective_group(group)
# __gloo_group_destroy_end__
# __gloo_full_example_start__
import torch
import ray
from ray.experimental.collective import create_collective_group
@ray.remote
class MyActor:
@ray.method(tensor_transport="gloo")
def random_tensor(self):
return torch.randn(1000, 1000)
def sum(self, tensor: torch.Tensor):
return torch.sum(tensor)
sender, receiver = MyActor.remote(), MyActor.remote()
group = create_collective_group([sender, receiver], backend="torch_gloo")
# The tensor will be stored by the `sender` actor instead of in Ray's object
# store.
tensor = sender.random_tensor.remote()
result = receiver.sum.remote(tensor)
print(ray.get(result))
# __gloo_full_example_end__
# __gloo_multiple_tensors_example_start__
import torch
import ray
from ray.experimental.collective import create_collective_group
@ray.remote
class MyActor:
@ray.method(tensor_transport="gloo")
def random_tensor_dict(self):
return {"tensor1": torch.randn(1000, 1000), "tensor2": torch.randn(1000, 1000)}
def sum(self, tensor_dict: dict):
return torch.sum(tensor_dict["tensor1"]) + torch.sum(tensor_dict["tensor2"])
sender, receiver = MyActor.remote(), MyActor.remote()
group = create_collective_group([sender, receiver], backend="torch_gloo")
# Both tensor values in the dictionary will be stored by the `sender` actor
# instead of in Ray's object store.
tensor_dict = sender.random_tensor_dict.remote()
result = receiver.sum.remote(tensor_dict)
print(ray.get(result))
# __gloo_multiple_tensors_example_end__
# __gloo_intra_actor_start__
import torch
import ray
import pytest
from ray.experimental.collective import create_collective_group
@ray.remote
class MyActor:
@ray.method(tensor_transport="gloo")
def random_tensor(self):
return torch.randn(1000, 1000)
def sum(self, tensor: torch.Tensor):
return torch.sum(tensor)
sender, receiver = MyActor.remote(), MyActor.remote()
group = create_collective_group([sender, receiver], backend="torch_gloo")
tensor = sender.random_tensor.remote()
# Pass the ObjectRef back to the actor that produced it. The tensor will be
# passed back to the same actor without copying.
sum1 = sender.sum.remote(tensor)
sum2 = receiver.sum.remote(tensor)
assert torch.allclose(*ray.get([sum1, sum2]))
# __gloo_intra_actor_end__
# __gloo_get_start__
# Wrong example of ray.get(). Since the tensor transport in the @ray.method decorator is Gloo,
# ray.get() will try to use Gloo to fetch the tensor, which is not supported
# because the caller is not part of the collective group.
with pytest.raises(ValueError) as e:
ray.get(tensor)
assert (
"ray.get is not allowed on RDT objects using the two-sided transport"
in str(e.value)
)
# Correct example of ray.get(), using the object store to fetch the RDT object because the caller
# is not part of the collective group.
print(ray.get(tensor, _use_object_store=True))
# torch.Tensor(...)
# __gloo_get_end__
# __gloo_object_mutability_warning_start__
import torch
import ray
from ray.experimental.collective import create_collective_group
@ray.remote
class MyActor:
@ray.method(tensor_transport="gloo")
def random_tensor(self):
return torch.randn(1000, 1000)
def increment_and_sum(self, tensor: torch.Tensor):
# In-place update.
tensor += 1
return torch.sum(tensor)
sender, receiver = MyActor.remote(), MyActor.remote()
group = create_collective_group([sender, receiver], backend="torch_gloo")
tensor = sender.random_tensor.remote()
tensor1 = sender.increment_and_sum.remote(tensor)
tensor2 = receiver.increment_and_sum.remote(tensor)
# A warning will be printed:
# UserWarning: GPU ObjectRef(...) is being passed back to the actor that created it Actor(MyActor, ...). Note that GPU objects are mutable. If the tensor is modified, Ray's internal copy will also be updated, and subsequent passes to other actors will receive the updated version instead of the original.
try:
# This assertion may fail because the tensor returned by sender.random_tensor
# is modified in-place by sender.increment_and_sum while being sent to
# receiver.increment_and_sum.
assert torch.allclose(ray.get(tensor1), ray.get(tensor2))
except AssertionError:
print("AssertionError: sender and receiver returned different sums.")
# __gloo_object_mutability_warning_end__
# __gloo_wait_tensor_freed_bad_start__
import torch
import ray
from ray.experimental.collective import create_collective_group
@ray.remote
class MyActor:
@ray.method(tensor_transport="gloo")
def random_tensor(self):
self.tensor = torch.randn(1000, 1000)
# After this function returns, Ray and this actor will both hold a
# reference to the same tensor.
return self.tensor
def increment_and_sum_stored_tensor(self):
# NOTE: In-place update, while Ray still holds a reference to the same tensor.
self.tensor += 1
return torch.sum(self.tensor)
def increment_and_sum(self, tensor: torch.Tensor):
return torch.sum(tensor + 1)
sender, receiver = MyActor.remote(), MyActor.remote()
group = create_collective_group([sender, receiver], backend="torch_gloo")
tensor = sender.random_tensor.remote()
tensor1 = sender.increment_and_sum_stored_tensor.remote()
# Wait for sender.increment_and_sum_stored_tensor task to finish.
tensor1 = ray.get(tensor1)
# Receiver will now receive the updated value instead of the original.
tensor2 = receiver.increment_and_sum.remote(tensor)
try:
# This assertion will fail because sender.increment_and_sum_stored_tensor
# modified the tensor in place before sending it to
# receiver.increment_and_sum.
assert torch.allclose(tensor1, ray.get(tensor2))
except AssertionError:
print("AssertionError: sender and receiver returned different sums.")
# __gloo_wait_tensor_freed_bad_end__
# __gloo_wait_tensor_freed_start__
import torch
import ray
from ray.experimental.collective import create_collective_group
@ray.remote
class MyActor:
@ray.method(tensor_transport="gloo")
def random_tensor(self):
self.tensor = torch.randn(1000, 1000)
return self.tensor
def increment_and_sum_stored_tensor(self):
# 1. Sender actor waits for Ray to release all references to the tensor
# before modifying the tensor in place.
ray.experimental.wait_tensor_freed(self.tensor)
# NOTE: In-place update, but Ray guarantees that it has already released
# its references to this tensor.
self.tensor += 1
return torch.sum(self.tensor)
def increment_and_sum(self, tensor: torch.Tensor):
# Receiver task remains the same.
return torch.sum(tensor + 1)
sender, receiver = MyActor.remote(), MyActor.remote()
group = create_collective_group([sender, receiver], backend="torch_gloo")
tensor = sender.random_tensor.remote()
tensor1 = sender.increment_and_sum_stored_tensor.remote()
# 2. Skip `ray.get`` because `wait_tensor_freed`` will block until all
# references to `tensor` are freed, so calling `ray.get` here would cause a
# deadlock.
# tensor1 = ray.get(tensor1)
tensor2 = receiver.increment_and_sum.remote(tensor)
# 3. Delete all references to `tensor`, to unblock wait_tensor_freed.
del tensor
# This assertion will now pass.
assert torch.allclose(ray.get(tensor1), ray.get(tensor2))
# __gloo_wait_tensor_freed_end__
@@ -0,0 +1,27 @@
# flake8: noqa
# __nccl_full_example_start__
import torch
import ray
from ray.experimental.collective import create_collective_group
@ray.remote(num_gpus=1)
class MyActor:
@ray.method(tensor_transport="nccl")
def random_tensor(self):
return torch.randn(1000, 1000).cuda()
def sum(self, tensor: torch.Tensor):
return torch.sum(tensor)
sender, receiver = MyActor.remote(), MyActor.remote()
group = create_collective_group([sender, receiver], backend="nccl")
# The tensor will be stored by the `sender` actor instead of in Ray's object
# store.
tensor = sender.random_tensor.remote()
result = receiver.sum.remote(tensor)
ray.get(result)
# __nccl_full_example_end__
@@ -0,0 +1,91 @@
# flake8: noqa
# __nixl_full_example_start__
import torch
import ray
@ray.remote(num_gpus=1)
class MyActor:
@ray.method(tensor_transport="nixl")
def random_tensor(self):
return torch.randn(1000, 1000).cuda()
def sum(self, tensor: torch.Tensor):
return torch.sum(tensor)
def produce(self, tensors):
refs = []
for t in tensors:
refs.append(ray.put(t, _tensor_transport="nixl"))
return refs
def consume_with_nixl(self, refs):
# ray.get will also use NIXL to retrieve the
# result.
tensors = [ray.get(ref) for ref in refs]
sum = 0
for t in tensors:
assert t.device.type == "cuda"
sum += t.sum().item()
return sum
# No collective group is needed. The two actors just need to have NIXL
# installed.
sender, receiver = MyActor.remote(), MyActor.remote()
# The tensor will be stored by the `sender` actor instead of in Ray's object
# store.
tensor = sender.random_tensor.remote()
result = receiver.sum.remote(tensor)
ray.get(result)
# __nixl_full_example_end__
# __nixl_get_start__
# ray.get will also use NIXL to retrieve the
# result.
print(ray.get(tensor))
# torch.Tensor(...)
# __nixl_get_end__
# __nixl_put__and_get_start__
tensor1 = torch.randn(1000, 1000).cuda()
tensor2 = torch.randn(1000, 1000).cuda()
refs = sender.produce.remote([tensor1, tensor2])
ref1 = receiver.consume_with_nixl.remote(refs)
print(ray.get(ref1))
# __nixl_put__and_get_end__
# __nixl_limitations_start__
@ray.remote(num_gpus=1)
class Actor:
def __init__(self):
self.tensor1 = torch.tensor([1, 2, 3])
self.tensor2 = torch.tensor([4, 5, 6])
self.tensor3 = torch.tensor([7, 8, 9])
@ray.method(tensor_transport="nixl")
def send_dict1(self):
return {"round1-1": self.tensor1, "round1-2": self.tensor2}
@ray.method(tensor_transport="nixl")
def send_dict2(self):
return {"round2-1": self.tensor1, "round2-3": self.tensor3}
def sum_dict(self, dict):
return sum(v.sum().item() for v in dict.values())
sender, receiver = Actor.remote(), Actor.remote()
ref1 = sender.send_dict1.remote()
result1 = receiver.sum_dict.remote(ref1)
print(ray.get(result1))
ref2 = sender.send_dict2.remote()
result2 = receiver.sum_dict.remote(ref2)
try:
print(ray.get(result2))
except ValueError as e:
print("Error caught:", e)
# __nixl_limitations_end__
@@ -0,0 +1,88 @@
# __return_ray_put_start__
import ray
# Non-fault tolerant version:
@ray.remote
def a():
x_ref = ray.put(1)
return x_ref
x_ref = ray.get(a.remote())
# Object x outlives its owner task A.
try:
# If owner of x (i.e. the worker process running task A) dies,
# the application can no longer get value of x.
print(ray.get(x_ref))
except ray.exceptions.OwnerDiedError:
pass
# __return_ray_put_end__
# __return_directly_start__
# Fault tolerant version:
@ray.remote
def a():
# Here we return the value directly instead of calling ray.put() first.
return 1
# The owner of x is the driver
# so x is accessible and can be auto recovered
# during the entire lifetime of the driver.
x_ref = a.remote()
print(ray.get(x_ref))
# __return_directly_end__
# __node_ip_resource_start__
@ray.remote
def b():
return 1
# If the node with ip 127.0.0.3 fails while task b is running,
# Ray cannot retry the task on other nodes.
b.options(resources={"node:127.0.0.3": 1}).remote()
# __node_ip_resource_end__
# __node_affinity_scheduling_strategy_start__
# Prefer running on the particular node specified by node id
# but can also run on other nodes if the target node fails.
b.options(
scheduling_strategy=ray.util.scheduling_strategies.NodeAffinitySchedulingStrategy(
node_id=ray.get_runtime_context().get_node_id(), soft=True
)
).remote()
# __node_affinity_scheduling_strategy_end__
# __manual_retry_start__
@ray.remote
class Actor:
def read_only(self):
import sys
import random
rand = random.random()
if rand < 0.2:
return 2 / 0
elif rand < 0.3:
sys.exit(1)
return 2
actor = Actor.remote()
# Manually retry the actor task.
while True:
try:
print(ray.get(actor.read_only.remote()))
break
except ZeroDivisionError:
pass
except ray.exceptions.RayActorError:
# Manually restart the actor
actor = Actor.remote()
# __manual_retry_end__
+120
View File
@@ -0,0 +1,120 @@
# __program_start__
import ray
from ray import DynamicObjectRefGenerator
# fmt: off
# __dynamic_generator_start__
import numpy as np
@ray.remote(num_returns="dynamic")
def split(array, chunk_size):
while len(array) > 0:
yield array[:chunk_size]
array = array[chunk_size:]
array_ref = ray.put(np.zeros(np.random.randint(1000_000)))
block_size = 1000
# Returns an ObjectRef[DynamicObjectRefGenerator].
dynamic_ref = split.remote(array_ref, block_size)
print(dynamic_ref)
# ObjectRef(c8ef45ccd0112571ffffffffffffffffffffffff0100000001000000)
i = -1
ref_generator = ray.get(dynamic_ref)
print(ref_generator)
# <ray._raylet.DynamicObjectRefGenerator object at 0x7f7e2116b290>
for i, ref in enumerate(ref_generator):
# Each DynamicObjectRefGenerator iteration returns an ObjectRef.
assert len(ray.get(ref)) <= block_size
num_blocks_generated = i + 1
array_size = len(ray.get(array_ref))
assert array_size <= num_blocks_generated * block_size
print(f"Split array of size {array_size} into {num_blocks_generated} blocks of "
f"size {block_size} each.")
# Split array of size 63153 into 64 blocks of size 1000 each.
# NOTE: The dynamic_ref points to the generated ObjectRefs. Make sure that this
# ObjectRef goes out of scope so that Ray can garbage-collect the internal
# ObjectRefs.
del dynamic_ref
# __dynamic_generator_end__
# fmt: on
# fmt: off
# __dynamic_generator_pass_start__
@ray.remote
def get_size(ref_generator : DynamicObjectRefGenerator):
print(ref_generator)
num_elements = 0
for ref in ref_generator:
array = ray.get(ref)
assert len(array) <= block_size
num_elements += len(array)
return num_elements
# Returns an ObjectRef[DynamicObjectRefGenerator].
dynamic_ref = split.remote(array_ref, block_size)
assert array_size == ray.get(get_size.remote(dynamic_ref))
# (get_size pid=1504184)
# <ray._raylet.DynamicObjectRefGenerator object at 0x7f81c4250ad0>
# This also works, but should be avoided because you have to call an additional
# `ray.get`, which blocks the driver.
ref_generator = ray.get(dynamic_ref)
assert array_size == ray.get(get_size.remote(ref_generator))
# (get_size pid=1504184)
# <ray._raylet.DynamicObjectRefGenerator object at 0x7f81c4251b50>
# __dynamic_generator_pass_end__
# fmt: on
# fmt: off
# __generator_errors_start__
@ray.remote
def generator():
for i in range(2):
yield i
raise Exception("error")
ref1, ref2, ref3, ref4 = generator.options(num_returns=4).remote()
assert ray.get([ref1, ref2]) == [0, 1]
# All remaining ObjectRefs will contain the error.
try:
ray.get([ref3, ref4])
except Exception as error:
print(error)
dynamic_ref = generator.options(num_returns="dynamic").remote()
ref_generator = ray.get(dynamic_ref)
ref1, ref2, ref3 = ref_generator
assert ray.get([ref1, ref2]) == [0, 1]
# Generators with num_returns="dynamic" will store the exception in the final
# ObjectRef.
try:
ray.get(ref3)
except Exception as error:
print(error)
# __generator_errors_end__
# fmt: on
# fmt: off
# __generator_errors_unsupported_start__
# Generators that yield more values than expected currently do not throw an
# exception (the error is only logged).
# See https://github.com/ray-project/ray/issues/28689.
ref1, ref2 = generator.options(num_returns=2).remote()
assert ray.get([ref1, ref2]) == [0, 1]
"""
(generator pid=2375938) 2022-09-28 11:08:51,386 ERROR worker.py:755 --
Unhandled error: Task threw exception, but all return values already
created. This should only occur when using generator tasks.
...
"""
# __generator_errors_unsupported_end__
# fmt: on
@@ -0,0 +1,19 @@
import ray
@ray.remote
class Greeter:
def __init__(self, value):
self.value = value
def say_hello(self):
return self.value
# Actor `g1` doesn't yet exist, so it is created with the given args.
a = Greeter.options(name="g1", get_if_exists=True).remote("Old Greeting")
assert ray.get(a.say_hello.remote()) == "Old Greeting"
# Actor `g1` already exists, so it is returned (new args are ignored).
b = Greeter.options(name="g1", get_if_exists=True).remote("New Greeting")
assert ray.get(b.say_hello.remote()) == "Old Greeting"
@@ -0,0 +1,75 @@
# flake8: noqa
# __starting_ray_start__
import ray
ray.init()
# __starting_ray_end__
# fmt: off
# __running_task_start__
# Define the square task.
@ray.remote
def square(x):
return x * x
# Launch four parallel square tasks.
futures = [square.remote(i) for i in range(4)]
# Retrieve results.
print(ray.get(futures))
# -> [0, 1, 4, 9]
# __running_task_end__
# fmt: on
# fmt: off
# __calling_actor_start__
# Define the Counter actor.
@ray.remote
class Counter:
def __init__(self):
self.i = 0
def get(self):
return self.i
def incr(self, value):
self.i += value
# Create a Counter actor.
c = Counter.remote()
# Submit calls to the actor. These calls run asynchronously but in
# submission order on the remote actor process.
for _ in range(10):
c.incr.remote(1)
# Retrieve final actor state.
print(ray.get(c.get.remote()))
# -> 10
# __calling_actor_end__
# fmt: on
# fmt: off
# __passing_object_start__
import numpy as np
# Define a task that sums the values in a matrix.
@ray.remote
def sum_matrix(matrix):
return np.sum(matrix)
# Call the task with a literal argument value.
print(ray.get(sum_matrix.remote(np.ones((100, 100)))))
# -> 10000.0
# Put a large array into the object store.
matrix_ref = ray.put(np.ones((1000, 1000)))
# Call the task with the object reference as an argument.
print(ray.get(sum_matrix.remote(matrix_ref)))
# -> 1000000.0
# __passing_object_end__
# fmt: on
@@ -0,0 +1,38 @@
# __without_backpressure_start__
import ray
ray.init()
@ray.remote
class Actor:
async def heavy_compute(self):
# taking a long time...
# await asyncio.sleep(5)
return
actor = Actor.remote()
NUM_TASKS = 1000
result_refs = []
# When NUM_TASKS is large enough, this will eventually OOM.
for _ in range(NUM_TASKS):
result_refs.append(actor.heavy_compute.remote())
ray.get(result_refs)
# __without_backpressure_end__
# __with_backpressure_start__
MAX_NUM_PENDING_TASKS = 100
result_refs = []
for _ in range(NUM_TASKS):
if len(result_refs) > MAX_NUM_PENDING_TASKS:
# update result_refs to only
# track the remaining tasks.
ready_refs, result_refs = ray.wait(result_refs, num_returns=1)
ray.get(ready_refs)
result_refs.append(actor.heavy_compute.remote())
ray.get(result_refs)
# __with_backpressure_end__
@@ -0,0 +1,35 @@
# __without_limit_start__
import ray
# Assume this Ray node has 16 CPUs and 16G memory.
ray.init()
@ray.remote
def process(file):
# Actual work is reading the file and process the data.
# Assume it needs to use 2G memory.
pass
NUM_FILES = 1000
result_refs = []
for i in range(NUM_FILES):
# By default, process task will use 1 CPU resource and no other resources.
# This means 16 tasks can run concurrently
# and will OOM since 32G memory is needed while the node only has 16G.
result_refs.append(process.remote(f"{i}.csv"))
ray.get(result_refs)
# __without_limit_end__
# __with_limit_start__
result_refs = []
for i in range(NUM_FILES):
# Now each task will use 2G memory resource
# and the number of concurrently running tasks is limited to 8.
# In this case, setting num_cpus to 2 has the same effect.
result_refs.append(
process.options(memory=2 * 1024 * 1024 * 1024).remote(f"{i}.csv")
)
ray.get(result_refs)
# __with_limit_end__
@@ -0,0 +1,90 @@
# __starting_ray_start__
import ray
import math
import time
import random
ray.init()
# __starting_ray_end__
# fmt: off
# __defining_actor_start__
@ray.remote
class ProgressActor:
def __init__(self, total_num_samples: int):
self.total_num_samples = total_num_samples
self.num_samples_completed_per_task = {}
def report_progress(self, task_id: int, num_samples_completed: int) -> None:
self.num_samples_completed_per_task[task_id] = num_samples_completed
def get_progress(self) -> float:
return (
sum(self.num_samples_completed_per_task.values()) / self.total_num_samples
)
# __defining_actor_end__
# fmt: on
# fmt: off
# __defining_task_start__
@ray.remote
def sampling_task(num_samples: int, task_id: int,
progress_actor: ray.actor.ActorHandle) -> int:
num_inside = 0
for i in range(num_samples):
x, y = random.uniform(-1, 1), random.uniform(-1, 1)
if math.hypot(x, y) <= 1:
num_inside += 1
# Report progress every 1 million samples.
if (i + 1) % 1_000_000 == 0:
# This is async.
progress_actor.report_progress.remote(task_id, i + 1)
# Report the final progress.
progress_actor.report_progress.remote(task_id, num_samples)
return num_inside
# __defining_task_end__
# fmt: on
# __creating_actor_start__
# Change this to match your cluster scale.
NUM_SAMPLING_TASKS = 10
NUM_SAMPLES_PER_TASK = 10_000_000
TOTAL_NUM_SAMPLES = NUM_SAMPLING_TASKS * NUM_SAMPLES_PER_TASK
# Create the progress actor.
progress_actor = ProgressActor.remote(TOTAL_NUM_SAMPLES)
# __creating_actor_end__
# __executing_task_start__
# Create and execute all sampling tasks in parallel.
results = [
sampling_task.remote(NUM_SAMPLES_PER_TASK, i, progress_actor)
for i in range(NUM_SAMPLING_TASKS)
]
# __executing_task_end__
# __calling_actor_start__
# Query progress periodically.
while True:
progress = ray.get(progress_actor.get_progress.remote())
print(f"Progress: {int(progress * 100)}%")
if progress == 1:
break
time.sleep(1)
# __calling_actor_end__
# __calculating_pi_start__
# Get all the sampling tasks results.
total_num_inside = sum(ray.get(results))
pi = (total_num_inside * 4) / TOTAL_NUM_SAMPLES
print(f"Estimated value of π is: {pi}")
# __calculating_pi_end__
assert str(pi).startswith("3.14")
+127
View File
@@ -0,0 +1,127 @@
# flake8: noqa
# fmt: off
# __init_namespace_start__
import ray
ray.init(namespace="hello")
# __init_namespace_end__
# fmt: on
ray.shutdown()
# fmt: off
# __actor_namespace_start__
import subprocess
import ray
try:
subprocess.check_output(["ray", "start", "--head"])
@ray.remote
class Actor:
pass
# Job 1 creates two actors, "orange" and "purple" in the "colors" namespace.
with ray.init("ray://localhost:10001", namespace="colors"):
Actor.options(name="orange", lifetime="detached").remote()
Actor.options(name="purple", lifetime="detached").remote()
# Job 2 is now connecting to a different namespace.
with ray.init("ray://localhost:10001", namespace="fruits"):
# This fails because "orange" was defined in the "colors" namespace.
try:
ray.get_actor("orange")
except ValueError:
pass
# This succeeds because the name "orange" is unused in this namespace.
Actor.options(name="orange", lifetime="detached").remote()
Actor.options(name="watermelon", lifetime="detached").remote()
# Job 3 connects to the original "colors" namespace
context = ray.init("ray://localhost:10001", namespace="colors")
# This fails because "watermelon" was in the fruits namespace.
try:
ray.get_actor("watermelon")
except ValueError:
pass
# This returns the "orange" actor we created in the first job, not the second.
ray.get_actor("orange")
# We are manually managing the scope of the connection in this example.
context.disconnect()
finally:
subprocess.check_output(["ray", "stop", "--force"])
# __actor_namespace_end__
# fmt: on
# fmt: off
# __specify_actor_namespace_start__
import subprocess
import ray
try:
subprocess.check_output(["ray", "start", "--head"])
@ray.remote
class Actor:
pass
ctx = ray.init("ray://localhost:10001")
# Create an actor with specified namespace.
Actor.options(name="my_actor", namespace="actor_namespace", lifetime="detached").remote()
# It is accessible in its namespace.
ray.get_actor("my_actor", namespace="actor_namespace")
ctx.disconnect()
finally:
subprocess.check_output(["ray", "stop", "--force"])
# __specify_actor_namespace_end__
# fmt: on
# fmt: off
# __anonymous_namespace_start__
import subprocess
import ray
try:
subprocess.check_output(["ray", "start", "--head"])
@ray.remote
class Actor:
pass
# Job 1 connects to an anonymous namespace by default
with ray.init("ray://localhost:10001"):
Actor.options(name="my_actor", lifetime="detached").remote()
# Job 2 connects to a _different_ anonymous namespace by default
with ray.init("ray://localhost:10001"):
# This succeeds because the second job is in its own namespace.
Actor.options(name="my_actor", lifetime="detached").remote()
finally:
subprocess.check_output(["ray", "stop", "--force"])
# __anonymous_namespace_end__
# fmt: on
# fmt: off
# __get_namespace_start__
import subprocess
import ray
try:
subprocess.check_output(["ray", "start", "--head"])
ray.init(address="auto", namespace="colors")
# Will print namespace name "colors".
print(ray.get_runtime_context().namespace)
finally:
subprocess.check_output(["ray", "stop", "--force"])
# __get_namespace_end__
# fmt: on
@@ -0,0 +1,42 @@
# flake8: noqa
# __nested_start__
import ray
@ray.remote
def f():
return 1
@ray.remote
def g():
# Call f 4 times and return the resulting object refs.
return [f.remote() for _ in range(4)]
@ray.remote
def h():
# Call f 4 times, block until those 4 tasks finish,
# retrieve the results, and return the values.
return ray.get([f.remote() for _ in range(4)])
# __nested_end__
ray.init(num_cpus=4, num_gpus=1)
obj_refs = ray.get(g.remote())
assert len(obj_refs) == 4
assert all(isinstance(o, ray.ObjectRef) for o in obj_refs)
# __yield_start__
@ray.remote(num_cpus=1, num_gpus=1)
def g():
return ray.get(f.remote())
# __yield_end__
assert ray.get(g.remote()) == 1
@@ -0,0 +1,16 @@
import ray
# Put the values (1, 2, 3) into Ray's object store.
a, b, c = ray.put(1), ray.put(2), ray.put(3)
@ray.remote
def print_via_capture():
"""This function prints the values of (a, b, c) to stdout."""
print(ray.get([a, b, c]))
# Passing object references via closure-capture. Inside the `print_via_capture`
# function, the global object refs (a, b, c) can be retrieved and printed.
print_via_capture.remote()
# -> prints [1, 2, 3]
+18
View File
@@ -0,0 +1,18 @@
import ray
@ray.remote
def echo_and_get(x_list): # List[ObjectRef]
"""This function prints its input values to stdout."""
print("args:", x_list)
print("values:", ray.get(x_list))
# Put the values (1, 2, 3) into Ray's object store.
a, b, c = ray.put(1), ray.put(2), ray.put(3)
# Passing an object as a nested argument to `echo_and_get`. Ray does not
# de-reference nested args, so `echo_and_get` sees the references.
echo_and_get.remote([a, b, c])
# -> prints args: [ObjectRef(...), ObjectRef(...), ObjectRef(...)]
# values: [1, 2, 3]
+20
View File
@@ -0,0 +1,20 @@
import ray
@ray.remote
def echo(a: int, b: int, c: int):
"""This function prints its input values to stdout."""
print(a, b, c)
# Passing the literal values (1, 2, 3) to `echo`.
echo.remote(1, 2, 3)
# -> prints "1 2 3"
# Put the values (1, 2, 3) into Ray's object store.
a, b, c = ray.put(1), ray.put(2), ray.put(3)
# Passing an object as a top-level argument to `echo`. Ray will de-reference top-level
# arguments, so `echo` will see the literal values (1, 2, 3) in this case as well.
echo.remote(a, b, c)
# -> prints "1 2 3"
@@ -0,0 +1,25 @@
import ray
from ray import cloudpickle
FILE = "external_store.pickle"
ray.init()
my_dict = {"hello": "world"}
obj_ref = ray.put(my_dict)
with open(FILE, "wb+") as f:
cloudpickle.dump(obj_ref, f)
# ObjectRef remains pinned in memory because
# it was serialized with ray.cloudpickle.
del obj_ref
with open(FILE, "rb") as f:
new_obj_ref = cloudpickle.load(f)
# The deserialized ObjectRef works as expected.
assert ray.get(new_obj_ref) == my_dict
# Explicitly free the object.
ray._private.internal_api.free(new_obj_ref)
@@ -0,0 +1,28 @@
import ray
from ray.util.placement_group import (
placement_group,
)
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
# Two "CPU"s are available.
ray.init(num_cpus=2)
# Create a placement group.
pg = placement_group([{"CPU": 2}])
ray.get(pg.ready())
# Now, 2 CPUs are not available anymore because
# they are pre-reserved by the placement group.
@ray.remote(num_cpus=2)
def f():
return True
# Won't be scheduled because there are no 2 cpus.
f.remote()
# Will be scheduled because 2 cpus are reserved by the placement group.
f.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg)
).remote()
+19
View File
@@ -0,0 +1,19 @@
# flake8: noqa
# __owners_begin__
import ray
import numpy as np
@ray.remote
def large_array():
return np.zeros(int(1e5))
x = ray.put(1) # The driver owns x and also creates the value of x.
y = large_array.remote()
# The driver is the owner of y, even though the value may be stored somewhere else.
# If the node that stores the value of y dies, Ray will automatically recover
# it by re-executing the large_array task.
# If the driver dies, anyone still using y will receive an OwnerDiedError.
# __owners_end__
@@ -0,0 +1,70 @@
# __sync_actor_start__
import ray
@ray.remote
class TaskStore:
def get_next_task(self):
return "task"
@ray.remote
class TaskExecutor:
def __init__(self, task_store):
self.task_store = task_store
self.num_executed_tasks = 0
def run(self):
while True:
task = ray.get(self.task_store.get_next_task.remote())
self._execute_task(task)
def _execute_task(self, task):
# Executing the task
self.num_executed_tasks = self.num_executed_tasks + 1
def get_num_executed_tasks(self):
return self.num_executed_tasks
task_store = TaskStore.remote()
task_executor = TaskExecutor.remote(task_store)
task_executor.run.remote()
try:
# This will timeout since task_executor.run occupies the entire actor thread
# and get_num_executed_tasks cannot run.
ray.get(task_executor.get_num_executed_tasks.remote(), timeout=5)
except ray.exceptions.GetTimeoutError:
print("get_num_executed_tasks didn't finish in 5 seconds")
# __sync_actor_end__
# __async_actor_start__
@ray.remote
class AsyncTaskExecutor:
def __init__(self, task_store):
self.task_store = task_store
self.num_executed_tasks = 0
async def run(self):
while True:
# Here we use await instead of ray.get() to
# wait for the next task and it will yield
# the control while waiting.
task = await self.task_store.get_next_task.remote()
self._execute_task(task)
def _execute_task(self, task):
# Executing the task
self.num_executed_tasks = self.num_executed_tasks + 1
def get_num_executed_tasks(self):
return self.num_executed_tasks
async_task_executor = AsyncTaskExecutor.remote(task_store)
async_task_executor.run.remote()
# We are able to run get_num_executed_tasks while run method is running.
num_executed_tasks = ray.get(async_task_executor.get_num_executed_tasks.remote())
print(f"num of executed tasks so far: {num_executed_tasks}")
# __async_actor_end__
@@ -0,0 +1,61 @@
import sys
if len(sys.argv) == 1:
# A small number for CI test.
sys.argv.append("5")
# __program_start__
import sys
import ray
# fmt: off
# __large_values_start__
import numpy as np
@ray.remote
def large_values(num_returns):
return [
np.random.randint(np.iinfo(np.int8).max, size=(100_000_000, 1), dtype=np.int8)
for _ in range(num_returns)
]
# __large_values_end__
# fmt: on
# fmt: off
# __large_values_generator_start__
@ray.remote
def large_values_generator(num_returns):
for i in range(num_returns):
yield np.random.randint(
np.iinfo(np.int8).max, size=(100_000_000, 1), dtype=np.int8
)
print(f"yielded return value {i}")
# __large_values_generator_end__
# fmt: on
# A large enough value (e.g. 100).
num_returns = int(sys.argv[1])
# Worker will likely OOM using normal returns.
print("Using normal functions...")
try:
ray.get(
large_values.options(num_returns=num_returns, max_retries=0).remote(
num_returns
)[0]
)
except ray.exceptions.WorkerCrashedError:
print("Worker failed with normal function")
# Using a generator will allow the worker to finish.
# Note that this will block until the full task is complete, i.e. the
# last yield finishes.
print("Using generators...")
ray.get(
large_values_generator.options(num_returns=num_returns, max_retries=0).remote(
num_returns
)[0]
)
print("Success!")
@@ -0,0 +1,72 @@
# __pattern_start__
import ray
import time
from numpy import random
def partition(collection):
# Use the last element as the pivot
pivot = collection.pop()
greater, lesser = [], []
for element in collection:
if element > pivot:
greater.append(element)
else:
lesser.append(element)
return lesser, pivot, greater
def quick_sort(collection):
if len(collection) <= 200000: # magic number
return sorted(collection)
else:
lesser, pivot, greater = partition(collection)
lesser = quick_sort(lesser)
greater = quick_sort(greater)
return lesser + [pivot] + greater
@ray.remote
def quick_sort_distributed(collection):
# Tiny tasks are an antipattern.
# Thus, in our example we have a "magic number" to
# toggle when distributed recursion should be used vs
# when the sorting should be done in place. The rule
# of thumb is that the duration of an individual task
# should be at least 1 second.
if len(collection) <= 200000: # magic number
return sorted(collection)
else:
lesser, pivot, greater = partition(collection)
lesser = quick_sort_distributed.remote(lesser)
greater = quick_sort_distributed.remote(greater)
return ray.get(lesser) + [pivot] + ray.get(greater)
for size in [200000, 4000000, 8000000]:
print(f"Array size: {size}")
unsorted = random.randint(1000000, size=(size)).tolist()
s = time.time()
quick_sort(unsorted)
print(f"Sequential execution: {(time.time() - s):.3f}")
s = time.time()
ray.get(quick_sort_distributed.remote(unsorted))
print(f"Distributed execution: {(time.time() - s):.3f}")
print("--" * 10)
# Outputs:
# Array size: 200000
# Sequential execution: 0.040
# Distributed execution: 0.152
# --------------------
# Array size: 4000000
# Sequential execution: 6.161
# Distributed execution: 5.779
# --------------------
# Array size: 8000000
# Sequential execution: 15.459
# Distributed execution: 11.282
# --------------------
# __pattern_end__
@@ -0,0 +1,66 @@
import ray
@ray.remote
class WorkQueue:
def __init__(self):
self.queue = list(range(10))
def get_work_item(self):
if self.queue:
return self.queue.pop(0)
else:
return None
@ray.remote
class WorkerWithoutPipelining:
def __init__(self, work_queue):
self.work_queue = work_queue
def process(self, work_item):
print(work_item)
def run(self):
while True:
# Get work from the remote queue.
work_item = ray.get(self.work_queue.get_work_item.remote())
if work_item is None:
break
# Do work.
self.process(work_item)
@ray.remote
class WorkerWithPipelining:
def __init__(self, work_queue):
self.work_queue = work_queue
def process(self, work_item):
print(work_item)
def run(self):
self.work_item_ref = self.work_queue.get_work_item.remote()
while True:
# Get work from the remote queue.
work_item = ray.get(self.work_item_ref)
if work_item is None:
break
self.work_item_ref = self.work_queue.get_work_item.remote()
# Do work while we are fetching the next work item.
self.process(work_item)
work_queue = WorkQueue.remote()
worker_without_pipelining = WorkerWithoutPipelining.remote(work_queue)
ray.get(worker_without_pipelining.run.remote())
work_queue = WorkQueue.remote()
worker_with_pipelining = WorkerWithPipelining.remote(work_queue)
ray.get(worker_with_pipelining.run.remote())
@@ -0,0 +1,32 @@
import ray
@ray.remote(num_cpus=1)
class Trainer:
def __init__(self, hyperparameter, data):
self.hyperparameter = hyperparameter
self.data = data
# Train the model on the given training data shard.
def fit(self):
return self.data * self.hyperparameter
@ray.remote(num_cpus=1)
class Supervisor:
def __init__(self, hyperparameter, data):
self.trainers = [Trainer.remote(hyperparameter, d) for d in data]
def fit(self):
# Train with different data shard in parallel.
return ray.get([trainer.fit.remote() for trainer in self.trainers])
data = [1, 2, 3]
supervisor1 = Supervisor.remote(1, data)
supervisor2 = Supervisor.remote(2, data)
# Train with different hyperparameters in parallel.
model1 = supervisor1.fit.remote()
model2 = supervisor2.fit.remote()
assert ray.get(model1) == [1, 2, 3]
assert ray.get(model2) == [2, 4, 6]
@@ -0,0 +1,68 @@
# __child_capture_pg_start__
import ray
from ray.util.placement_group import placement_group
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
ray.init(num_cpus=2)
# Create a placement group.
pg = placement_group([{"CPU": 2}])
ray.get(pg.ready())
@ray.remote(num_cpus=1)
def child():
import time
time.sleep(5)
@ray.remote(num_cpus=1)
def parent():
# The child task is scheduled to the same placement group as its parent,
# although it didn't specify the PlacementGroupSchedulingStrategy.
ray.get(child.remote())
# Since the child and parent use 1 CPU each, the placement group
# bundle {"CPU": 2} is fully occupied.
ray.get(
parent.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=pg, placement_group_capture_child_tasks=True
)
).remote()
)
# __child_capture_pg_end__
# __child_capture_disable_pg_start__
@ray.remote
def parent():
# In this case, the child task isn't
# scheduled with the parent's placement group.
ray.get(
child.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=None)
).remote()
)
# This times out because we cannot schedule the child task.
# The cluster has {"CPU": 2}, and both of them are reserved by
# the placement group with a bundle {"CPU": 2}. Since the child shouldn't
# be scheduled within this placement group, it cannot be scheduled because
# there's no available CPU resources.
try:
ray.get(
parent.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=pg, placement_group_capture_child_tasks=True
)
).remote(),
timeout=5,
)
except Exception as e:
print("Couldn't create a child task!")
print(e)
# __child_capture_disable_pg_end__
@@ -0,0 +1,141 @@
# __create_pg_start__
from pprint import pprint
import time
# Import placement group APIs.
from ray.util.placement_group import (
placement_group,
placement_group_table,
remove_placement_group,
)
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
# Initialize Ray.
import ray
# Create a single node Ray cluster with 2 CPUs and 2 GPUs.
ray.init(num_cpus=2, num_gpus=2)
# Reserve a placement group of 1 bundle that reserves 1 CPU and 1 GPU.
pg = placement_group([{"CPU": 1, "GPU": 1}])
# __create_pg_end__
# __ready_pg_start__
# Wait until placement group is created.
ray.get(pg.ready(), timeout=10)
# You can also use ray.wait.
ready, unready = ray.wait([pg.ready()], timeout=10)
# You can look at placement group states using this API.
print(placement_group_table(pg))
# __ready_pg_end__
# __create_pg_failed_start__
# Cannot create this placement group because we
# cannot create a {"GPU": 2} bundle.
pending_pg = placement_group([{"CPU": 1}, {"GPU": 2}])
# This raises the timeout exception!
try:
ray.get(pending_pg.ready(), timeout=5)
except Exception as e:
print(
"Cannot create a placement group because "
"{'GPU': 2} bundle cannot be created."
)
print(e)
# __create_pg_failed_end__
# __schedule_pg_start__
@ray.remote(num_cpus=1)
class Actor:
def __init__(self):
pass
def ready(self):
pass
# Create an actor to a placement group.
actor = Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=pg,
)
).remote()
# Verify the actor is scheduled.
ray.get(actor.ready.remote(), timeout=10)
# __schedule_pg_end__
# __schedule_pg_3_start__
@ray.remote(num_cpus=0, num_gpus=1)
class Actor:
def __init__(self):
pass
def ready(self):
pass
# Create a GPU actor on the first bundle of index 0.
actor2 = Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=pg,
placement_group_bundle_index=0,
)
).remote()
# Verify that the GPU actor is scheduled.
ray.get(actor2.ready.remote(), timeout=10)
# __schedule_pg_3_end__
# __remove_pg_start__
# This API is asynchronous.
remove_placement_group(pg)
# Wait until placement group is killed.
time.sleep(1)
# Check that the placement group has died.
pprint(placement_group_table(pg))
"""
{'bundles': {0: {'GPU': 1.0}, 1: {'CPU': 1.0}},
'name': 'unnamed_group',
'placement_group_id': '40816b6ad474a6942b0edb45809b39c3',
'state': 'REMOVED',
'strategy': 'PACK'}
"""
# __remove_pg_end__
# __strategy_pg_start__
# Reserve a placement group of 2 bundles
# that have to be packed on the same node.
pg = placement_group([{"CPU": 1}, {"GPU": 1}], strategy="PACK")
# __strategy_pg_end__
remove_placement_group(pg)
# __detached_pg_start__
# driver_1.py
# Create a detached placement group that survives even after
# the job terminates.
pg = placement_group([{"CPU": 1}], lifetime="detached", name="global_name")
ray.get(pg.ready())
# __detached_pg_end__
remove_placement_group(pg)
# __get_pg_start__
# first_driver.py
# Create a placement group with a unique name within a namespace.
# Start Ray or connect to a Ray cluster using: ray.init(namespace="pg_namespace")
pg = placement_group([{"CPU": 1}], name="pg_name")
ray.get(pg.ready())
# second_driver.py
# Retrieve a placement group with a unique name within a namespace.
# Start Ray or connect to a Ray cluster using: ray.init(namespace="pg_namespace")
pg = ray.util.get_placement_group("pg_name")
# __get_pg_end__
+192
View File
@@ -0,0 +1,192 @@
# flake8: noqa
# fmt: off
# __dag_tasks_begin__
import ray
ray.init()
@ray.remote
def func(src, inc=1):
return src + inc
a_ref = func.bind(1, inc=2)
assert ray.get(a_ref.execute()) == 3 # 1 + 2 = 3
b_ref = func.bind(a_ref, inc=3)
assert ray.get(b_ref.execute()) == 6 # (1 + 2) + 3 = 6
c_ref = func.bind(b_ref, inc=a_ref)
assert ray.get(c_ref.execute()) == 9 # ((1 + 2) + 3) + (1 + 2) = 9
# __dag_tasks_end__
# fmt: on
ray.shutdown()
# fmt: off
# __dag_actors_begin__
import ray
ray.init()
@ray.remote
class Actor:
def __init__(self, init_value):
self.i = init_value
def inc(self, x):
self.i += x
def get(self):
return self.i
a1 = Actor.bind(10) # Instantiate Actor with init_value 10.
val = a1.get.bind() # ClassMethod that returns value from get() from
# the actor created.
assert ray.get(val.execute()) == 10
@ray.remote
def combine(x, y):
return x + y
a2 = Actor.bind(10) # Instantiate another Actor with init_value 10.
a1.inc.bind(2) # Call inc() on the actor created with increment of 2.
a1.inc.bind(4) # Call inc() on the actor created with increment of 4.
a2.inc.bind(6) # Call inc() on the actor created with increment of 6.
# Combine outputs from a1.get() and a2.get()
dag = combine.bind(a1.get.bind(), a2.get.bind())
# a1 + a2 + inc(2) + inc(4) + inc(6)
# 10 + (10 + ( 2 + 4 + 6)) = 32
assert ray.get(dag.execute()) == 32
# __dag_actors_end__
# fmt: on
ray.shutdown()
# fmt: off
# __dag_input_node_begin__
import ray
ray.init()
from ray.dag.input_node import InputNode
@ray.remote
def a(user_input):
return user_input * 2
@ray.remote
def b(user_input):
return user_input + 1
@ray.remote
def c(x, y):
return x + y
with InputNode() as dag_input:
a_ref = a.bind(dag_input)
b_ref = b.bind(dag_input)
dag = c.bind(a_ref, b_ref)
# a(2) + b(2) = c
# (2 * 2) + (2 + 1)
assert ray.get(dag.execute(2)) == 7
# a(3) + b(3) = c
# (3 * 2) + (3 + 1)
assert ray.get(dag.execute(3)) == 10
# __dag_input_node_end__
# fmt: on
# fmt: off
# __dag_multi_output_node_begin__
import ray
from ray.dag.input_node import InputNode
from ray.dag.output_node import MultiOutputNode
@ray.remote
def f(input):
return input + 1
with InputNode() as input_data:
dag = MultiOutputNode([f.bind(input_data["x"]), f.bind(input_data["y"])])
refs = dag.execute({"x": 1, "y": 2})
assert ray.get(refs) == [2, 3]
# __dag_multi_output_node_end__
# fmt: on
# fmt: off
# __dag_multi_output_node_begin__
import ray
from ray.dag.input_node import InputNode
from ray.dag.output_node import MultiOutputNode
@ray.remote
def f(input):
return input + 1
with InputNode() as input_data:
dag = MultiOutputNode([f.bind(input_data["x"]), f.bind(input_data["y"])])
refs = dag.execute({"x": 1, "y": 2})
assert ray.get(refs) == [2, 3]
# __dag_multi_output_node_end__
# fmt: on
# fmt: off
# __dag_multi_output_node_begin__
import ray
from ray.dag.input_node import InputNode
from ray.dag.output_node import MultiOutputNode
@ray.remote
def f(input):
return input + 1
with InputNode() as input_data:
dag = MultiOutputNode([f.bind(input_data["x"]), f.bind(input_data["y"])])
refs = dag.execute({"x": 1, "y": 2})
assert ray.get(refs) == [2, 3]
# __dag_multi_output_node_end__
# fmt: on
# fmt: off
# __dag_actor_reuse_begin__
import ray
from ray.dag.input_node import InputNode
from ray.dag.output_node import MultiOutputNode
@ray.remote
class Worker:
def __init__(self):
self.forwarded = 0
def forward(self, input_data: int):
self.forwarded += 1
return input_data + 1
def num_forwarded(self):
return self.forwarded
# Create an actor via ``remote`` API not ``bind`` API to avoid
# killing actors when a DAG is finished.
worker = Worker.remote()
with InputNode() as input_data:
dag = MultiOutputNode([worker.forward.bind(input_data)])
# Actors are reused. The DAG definition doesn't include
# actor creation.
assert ray.get(dag.execute(1)) == [2]
assert ray.get(dag.execute(2)) == [3]
assert ray.get(dag.execute(3)) == [4]
# You can still use other actor methods via `remote` API.
assert ray.get(worker.num_forwarded.remote()) == 3
# __dag_actor_reuse_end__
# fmt: on
@@ -0,0 +1,87 @@
# flake8: noqa
import ray
ray.init(
_system_config={
"memory_usage_threshold": 0.4,
},
)
# fmt: off
# __last_task_start__
import ray
@ray.remote(max_retries=0)
def leaks_memory():
chunks = []
bits_to_allocate = 8 * 100 * 1024 * 1024 # ~100 MiB
while True:
chunks.append([0] * bits_to_allocate)
try:
ray.get(leaks_memory.remote())
except ray.exceptions.OutOfMemoryError as ex:
print("task failed with OutOfMemoryError, which is expected")
# __last_task_end__
# fmt: on
# fmt: off
# __two_actors_start__
from math import ceil
import ray
from ray._private.utils import (
get_system_memory,
) # do not use outside of this example as these are private methods.
from ray._private.utils import (
get_used_memory,
) # do not use outside of this example as these are private methods.
# estimates the number of bytes to allocate to reach the desired memory usage percentage.
def get_additional_bytes_to_reach_memory_usage_pct(pct: float) -> int:
used = get_used_memory()
total = get_system_memory()
bytes_needed = int(total * pct) - used
assert (
bytes_needed > 0
), "memory usage is already above the target. Increase the target percentage."
return bytes_needed
@ray.remote
class MemoryHogger:
def __init__(self):
self.allocations = []
def allocate(self, bytes_to_allocate: float) -> None:
# divide by 8 as each element in the array occupies 8 bytes
new_list = [0] * ceil(bytes_to_allocate / 8)
self.allocations.append(new_list)
first_actor = MemoryHogger.options(
max_restarts=1, max_task_retries=1, name="first_actor"
).remote()
second_actor = MemoryHogger.options(
max_restarts=0, max_task_retries=0, name="second_actor"
).remote()
# We want total bytes to exceed 40% threshold to trigger the memory monitor.
allocate_bytes = get_additional_bytes_to_reach_memory_usage_pct(0.5)
first_actor_task = first_actor.allocate.remote(0.9 * allocate_bytes)
second_actor_task = second_actor.allocate.remote(0.1* allocate_bytes)
error_thrown = False
try:
ray.get(first_actor_task)
except ray.exceptions.OutOfMemoryError as ex:
error_thrown = True
print("First started actor, which is retriable, was killed by the memory monitor.")
assert error_thrown
ray.get(second_actor_task)
print("Second started actor, which is not-retriable, finished.")
# __two_actors_end__
# fmt: on
+59
View File
@@ -0,0 +1,59 @@
import ray
# __specifying_node_resources_start__
# This will start a Ray node with 3 logical cpus, 4 logical gpus,
# 1 special_hardware resource and 1 custom_label resource.
ray.init(num_cpus=3, num_gpus=4, resources={"special_hardware": 1, "custom_label": 1})
# __specifying_node_resources_end__
# __specifying_resource_requirements_start__
# Specify the default resource requirements for this remote function.
@ray.remote(num_cpus=2, num_gpus=2, resources={"special_hardware": 1})
def func():
return 1
# You can override the default resource requirements.
func.options(num_cpus=3, num_gpus=1, resources={"special_hardware": 0}).remote()
@ray.remote(num_cpus=0, num_gpus=1)
class Actor:
pass
# You can override the default resource requirements for actors as well.
actor = Actor.options(num_cpus=1, num_gpus=0).remote()
# __specifying_resource_requirements_end__
# __specifying_fractional_resource_requirements_start__
@ray.remote(num_cpus=0.5)
def io_bound_task():
import time
time.sleep(1)
return 2
io_bound_task.remote()
@ray.remote(num_gpus=0.5)
class IOActor:
def ping(self):
import os
print(f"CUDA_VISIBLE_DEVICES: {os.environ['CUDA_VISIBLE_DEVICES']}")
# Two actors can share the same GPU.
io_actor1 = IOActor.remote()
io_actor2 = IOActor.remote()
ray.get(io_actor1.ping.remote())
ray.get(io_actor2.ping.remote())
# Output:
# (IOActor pid=96328) CUDA_VISIBLE_DEVICES: 1
# (IOActor pid=96329) CUDA_VISIBLE_DEVICES: 1
# __specifying_fractional_resource_requirements_end__
@@ -0,0 +1,81 @@
# flake8: noqa
"""
This file holds code for the runtime envs documentation.
FIXME: We switched our code formatter from YAPF to Black. Check if we can enable code
formatting on this module and update the paragraph below. See issue #21318.
It ignores yapf because yapf doesn't allow comments right after code blocks,
but we put comments right after code blocks to prevent large white spaces
in the documentation.
"""
import ray
# fmt: off
# __runtime_env_pip_def_start__
runtime_env = {
"pip": ["emoji"],
"env_vars": {"TF_WARNINGS": "none"}
}
# __runtime_env_pip_def_end__
# __strong_typed_api_runtime_env_pip_def_start__
from ray.runtime_env import RuntimeEnv
runtime_env = RuntimeEnv(
pip=["emoji"],
env_vars={"TF_WARNINGS": "none"}
)
# __strong_typed_api_runtime_env_pip_def_end__
# __ray_init_start__
# Option 1: Starting a single-node local Ray cluster or connecting to existing local cluster
ray.init(runtime_env=runtime_env)
# __ray_init_end__
@ray.remote
def f_job():
pass
@ray.remote
class Actor_job:
def g(self):
pass
ray.get(f_job.remote())
a = Actor_job.remote()
ray.get(a.g.remote())
ray.shutdown()
ray.init()
@ray.remote
def f():
pass
@ray.remote
class SomeClass:
pass
# __per_task_per_actor_start__
# Invoke a remote task that will run in a specified runtime environment.
f.options(runtime_env=runtime_env).remote()
# Instantiate an actor that will run in a specified runtime environment.
actor = SomeClass.options(runtime_env=runtime_env).remote()
# Specify a runtime environment in the task definition. Future invocations via
# `g.remote()` will use this runtime environment unless overridden by using
# `.options()` as above.
@ray.remote(runtime_env=runtime_env)
def g():
pass
# Specify a runtime environment in the actor definition. Future instantiations
# via `MyClass.remote()` will use this runtime environment unless overridden by
# using `.options()` as above.
@ray.remote(runtime_env=runtime_env)
class MyClass:
pass
# __per_task_per_actor_end__
+122
View File
@@ -0,0 +1,122 @@
import ray
ray.init(num_cpus=64)
# __default_scheduling_strategy_start__
@ray.remote
def func():
return 1
@ray.remote(num_cpus=1)
class Actor:
pass
# If unspecified, "DEFAULT" scheduling strategy is used.
func.remote()
actor = Actor.remote()
# Explicitly set scheduling strategy to "DEFAULT".
func.options(scheduling_strategy="DEFAULT").remote()
actor = Actor.options(scheduling_strategy="DEFAULT").remote()
# Zero-CPU (and no other resources) actors are randomly assigned to nodes.
actor = Actor.options(num_cpus=0).remote()
# __default_scheduling_strategy_end__
# __spread_scheduling_strategy_start__
@ray.remote(scheduling_strategy="SPREAD")
def spread_func():
return 2
@ray.remote(num_cpus=1)
class SpreadActor:
pass
# Spread tasks across the cluster.
[spread_func.remote() for _ in range(10)]
# Spread actors across the cluster.
actors = [SpreadActor.options(scheduling_strategy="SPREAD").remote() for _ in range(10)]
# __spread_scheduling_strategy_end__
# __node_affinity_scheduling_strategy_start__
@ray.remote
def node_affinity_func():
return ray.get_runtime_context().get_node_id()
@ray.remote(num_cpus=1)
class NodeAffinityActor:
pass
# Only run the task on the local node.
node_affinity_func.options(
scheduling_strategy=ray.util.scheduling_strategies.NodeAffinitySchedulingStrategy(
node_id=ray.get_runtime_context().get_node_id(),
soft=False,
)
).remote()
# Run the two node_affinity_func tasks on the same node if possible.
node_affinity_func.options(
scheduling_strategy=ray.util.scheduling_strategies.NodeAffinitySchedulingStrategy(
node_id=ray.get(node_affinity_func.remote()),
soft=True,
)
).remote()
# Only run the actor on the local node.
actor = NodeAffinityActor.options(
scheduling_strategy=ray.util.scheduling_strategies.NodeAffinitySchedulingStrategy(
node_id=ray.get_runtime_context().get_node_id(),
soft=False,
)
).remote()
# __node_affinity_scheduling_strategy_end__
# __locality_aware_scheduling_start__
@ray.remote
def large_object_func():
# Large object is stored in the local object store
# and available in the distributed memory,
# instead of returning inline directly to the caller.
return [1] * (1024 * 1024)
@ray.remote
def small_object_func():
# Small object is returned inline directly to the caller,
# instead of storing in the distributed memory.
return [1]
@ray.remote
def consume_func(data):
return len(data)
large_object = large_object_func.remote()
small_object = small_object_func.remote()
# Ray will try to run consume_func on the same node
# where large_object_func runs.
consume_func.remote(large_object)
# Ray will try to spread consume_func across the entire cluster
# instead of only running on the node where large_object_func runs.
[
consume_func.options(scheduling_strategy="SPREAD").remote(large_object)
for i in range(10)
]
# Ray won't consider locality for scheduling consume_func
# since the argument is small and will be sent to the worker node inline directly.
consume_func.remote(small_object)
# __locality_aware_scheduling_end__
@@ -0,0 +1,209 @@
# flake8: noqa
# fmt: off
# __streaming_generator_define_start__
import ray
import time
@ray.remote
def task():
for i in range(5):
time.sleep(5)
yield i
# __streaming_generator_define_end__
# __streaming_generator_execute_start__
gen = task.remote()
# Blocks for 5 seconds.
ref = next(gen)
# return 0
ray.get(ref)
# Blocks for 5 seconds.
ref = next(gen)
# Return 1
ray.get(ref)
# Returns 2~4 every 5 seconds.
for ref in gen:
print(ray.get(ref))
# __streaming_generator_execute_end__
# __streaming_generator_exception_start__
@ray.remote
def task():
for i in range(5):
time.sleep(1)
if i == 1:
raise ValueError
yield i
gen = task.remote()
# it's okay.
ray.get(next(gen))
# Raises an exception
try:
ray.get(next(gen))
except ValueError as e:
print(f"Exception is raised when i == 1 as expected {e}")
# __streaming_generator_exception_end__
# __streaming_generator_actor_model_start__
@ray.remote
class Actor:
def f(self):
for i in range(5):
yield i
@ray.remote
class AsyncActor:
async def f(self):
for i in range(5):
yield i
@ray.remote(max_concurrency=5)
class ThreadedActor:
def f(self):
for i in range(5):
yield i
actor = Actor.remote()
for ref in actor.f.remote():
print(ray.get(ref))
actor = AsyncActor.remote()
for ref in actor.f.remote():
print(ray.get(ref))
actor = ThreadedActor.remote()
for ref in actor.f.remote():
print(ray.get(ref))
# __streaming_generator_actor_model_end__
# __streaming_generator_asyncio_start__
import asyncio
@ray.remote
def task():
for i in range(5):
time.sleep(1)
yield i
async def main():
async for ref in task.remote():
print(await ref)
asyncio.run(main())
# __streaming_generator_asyncio_end__
# __streaming_generator_gc_start__
@ray.remote
def task():
for i in range(5):
time.sleep(1)
yield i
gen = task.remote()
ref1 = next(gen)
del gen
# __streaming_generator_gc_end__
# __streaming_generator_concurrency_asyncio_start__
import asyncio
@ray.remote
def task():
for i in range(5):
time.sleep(1)
yield i
async def async_task():
async for ref in task.remote():
print(await ref)
async def main():
t1 = async_task()
t2 = async_task()
await asyncio.gather(t1, t2)
asyncio.run(main())
# __streaming_generator_concurrency_asyncio_end__
# __streaming_generator_wait_simple_start__
@ray.remote
def task():
for i in range(5):
time.sleep(5)
yield i
gen = task.remote()
# Because it takes 5 seconds to make the first yield,
# with 0 timeout, the generator is unready.
ready, unready = ray.wait([gen], timeout=0)
print("timeout 0, nothing is ready.")
print(ready)
assert len(ready) == 0
assert len(unready) == 1
# Without a timeout argument, ray.wait waits until the given argument
# is ready. When a next item is ready, it returns.
ready, unready = ray.wait([gen])
print("Wait for 5 seconds. The next item is ready.")
assert len(ready) == 1
assert len(unready) == 0
next(gen)
# Because the second yield hasn't happened yet,
ready, unready = ray.wait([gen], timeout=0)
print("Wait for 0 seconds. The next item is not ready.")
print(ready, unready)
assert len(ready) == 0
assert len(unready) == 1
# __streaming_generator_wait_simple_end__
# __streaming_generator_wait_complex_start__
from ray._raylet import ObjectRefGenerator
@ray.remote
def generator_task():
for i in range(5):
time.sleep(5)
yield i
@ray.remote
def regular_task():
for i in range(5):
time.sleep(5)
return
gen = [generator_task.remote()]
ref = [regular_task.remote()]
ready, unready = [], [*gen, *ref]
result = []
while unready:
ready, unready = ray.wait(unready)
for r in ready:
if isinstance(r, ObjectRefGenerator):
try:
ref = next(r)
result.append(ray.get(ref))
except StopIteration:
pass
else:
unready.append(r)
else:
result.append(ray.get(r))
# __streaming_generator_wait_complex_end__
@@ -0,0 +1,105 @@
# flake8: noqa
# fmt: off
# __task_exceptions_begin__
import ray
@ray.remote
def f():
raise Exception("the real error")
@ray.remote
def g(x):
return
try:
ray.get(f.remote())
except ray.exceptions.RayTaskError as e:
print(e)
# ray::f() (pid=71867, ip=XXX.XX.XXX.XX)
# File "errors.py", line 5, in f
# raise Exception("the real error")
# Exception: the real error
try:
ray.get(g.remote(f.remote()))
except ray.exceptions.RayTaskError as e:
print(e)
# ray::g() (pid=73085, ip=128.32.132.47)
# At least one of the input arguments for this task could not be computed:
# ray.exceptions.RayTaskError: ray::f() (pid=73085, ip=XXX.XX.XXX.XX)
# File "errors.py", line 5, in f
# raise Exception("the real error")
# Exception: the real error
# __task_exceptions_end__
# __unserializable_exceptions_begin__
import threading
class UnserializableException(Exception):
def __init__(self):
self.lock = threading.Lock()
@ray.remote
def raise_unserializable_error():
raise UnserializableException
try:
ray.get(raise_unserializable_error.remote())
except ray.exceptions.RayTaskError as e:
print(e)
# ray::raise_unserializable_error() (pid=328577, ip=172.31.5.154)
# File "/home/ubuntu/ray/tmp~/main.py", line 25, in raise_unserializable_error
# raise UnserializableException
# UnserializableException
print(type(e.cause))
# <class 'ray.exceptions.RayError'>
print(e.cause)
# The original cause of the RayTaskError (<class '__main__.UnserializableException'>) isn't serializable: cannot pickle '_thread.lock' object. Overwriting the cause to a RayError.
# __unserializable_exceptions_end__
# __catch_user_exceptions_begin__
class MyException(Exception):
...
@ray.remote
def raises_my_exc():
raise MyException("a user exception")
try:
ray.get(raises_my_exc.remote())
except MyException as e:
print(e)
# ray::raises_my_exc() (pid=15329, ip=127.0.0.1)
# File "<$PWD>/task_exceptions.py", line 45, in raises_my_exc
# raise MyException("a user exception")
# MyException: a user exception
# __catch_user_exceptions_end__
# __catch_user_final_exceptions_begin__
class MyFinalException(Exception):
def __init_subclass__(cls, /, *args, **kwargs):
raise TypeError("Cannot subclass this little exception class.")
@ray.remote
def raises_my_final_exc():
raise MyFinalException("a *final* user exception")
try:
ray.get(raises_my_final_exc.remote())
except ray.exceptions.RayTaskError as e:
assert isinstance(e.cause, MyFinalException)
print(e)
# 2024-04-08 21:11:47,417 WARNING exceptions.py:177 -- User exception type <class '__main__.MyFinalException'> in RayTaskError can not be subclassed! This exception will be raised as RayTaskError only. You can use `ray_task_error.cause` to access the user exception. Failure in subclassing: Cannot subclass this little exception class.
# ray::raises_my_final_exc() (pid=88226, ip=127.0.0.1)
# File "<$PWD>/task_exceptions.py", line 66, in raises_my_final_exc
# raise MyFinalException("a *final* user exception")
# MyFinalException: a *final* user exception
print(type(e.cause))
# <class '__main__.MyFinalException'>
print(e.cause)
# a *final* user exception
# __catch_user_final_exceptions_end__
# fmt: on
+137
View File
@@ -0,0 +1,137 @@
# flake8: noqa
# __tasks_start__
import ray
import time
# A regular Python function.
def normal_function():
return 1
# By adding the `@ray.remote` decorator, a regular Python function
# becomes a Ray remote function.
@ray.remote
def my_function():
return 1
# To invoke this remote function, use the `remote` method.
# This will immediately return an object ref (a future) and then create
# a task that will be executed on a worker process.
obj_ref = my_function.remote()
# The result can be retrieved with ``ray.get``.
assert ray.get(obj_ref) == 1
@ray.remote
def slow_function():
time.sleep(10)
return 1
# Ray tasks are executed in parallel.
# All computation is performed in the background, driven by Ray's internal event loop.
for _ in range(4):
# This doesn't block.
slow_function.remote()
# __tasks_end__
# __pass_by_ref_start__
@ray.remote
def function_with_an_argument(value):
return value + 1
obj_ref1 = my_function.remote()
assert ray.get(obj_ref1) == 1
# You can pass an object ref as an argument to another Ray task.
obj_ref2 = function_with_an_argument.remote(obj_ref1)
assert ray.get(obj_ref2) == 2
# __pass_by_ref_end__
# __wait_start__
object_refs = [slow_function.remote() for _ in range(2)]
# Return as soon as one of the tasks finished execution.
ready_refs, remaining_refs = ray.wait(object_refs, num_returns=1, timeout=None)
# __wait_end__
# __multiple_returns_start__
# By default, a Ray task only returns a single Object Ref.
@ray.remote
def return_single():
return 0, 1, 2
object_ref = return_single.remote()
assert ray.get(object_ref) == (0, 1, 2)
# However, you can configure Ray tasks to return multiple Object Refs.
@ray.remote(num_returns=3)
def return_multiple():
return 0, 1, 2
object_ref0, object_ref1, object_ref2 = return_multiple.remote()
assert ray.get(object_ref0) == 0
assert ray.get(object_ref1) == 1
assert ray.get(object_ref2) == 2
# __multiple_returns_end__
# __generator_start__
@ray.remote(num_returns=3)
def return_multiple_as_generator():
for i in range(3):
yield i
# NOTE: Similar to normal functions, these objects will not be available
# until the full task is complete and all returns have been generated.
a, b, c = return_multiple_as_generator.remote()
# __generator_end__
# __cancel_start__
@ray.remote
def blocking_operation():
time.sleep(10e6)
obj_ref = blocking_operation.remote()
ray.cancel(obj_ref)
try:
ray.get(obj_ref)
except ray.exceptions.TaskCancelledError:
print("Object reference was cancelled.")
# __cancel_end__
# __resource_start__
# Specify required resources.
@ray.remote(num_cpus=4, num_gpus=2)
def my_function():
return 1
# Override the default resource requirements.
my_function.options(num_cpus=3).remote()
# __resource_end__
# __fraction_resource_start__
# Ray also supports fractional resource requirements.
@ray.remote(num_gpus=0.5)
def h():
return 1
# Ray support custom resources too.
@ray.remote(resources={"Custom": 1})
def f():
return 1
# __fraction_resource_end__
@@ -0,0 +1,91 @@
# flake8: noqa
# fmt: off
# __tasks_fault_tolerance_retries_begin__
import numpy as np
import os
import ray
import time
ray.init(ignore_reinit_error=True)
@ray.remote(max_retries=1)
def potentially_fail(failure_probability):
time.sleep(0.2)
if np.random.random() < failure_probability:
os._exit(0)
return 0
for _ in range(3):
try:
# If this task crashes, Ray will retry it up to one additional
# time. If either of the attempts succeeds, the call to ray.get
# below will return normally. Otherwise, it will raise an
# exception.
ray.get(potentially_fail.remote(0.5))
print('SUCCESS')
except ray.exceptions.WorkerCrashedError:
print('FAILURE')
# __tasks_fault_tolerance_retries_end__
# fmt: on
# fmt: off
# __tasks_fault_tolerance_retries_exception_begin__
import numpy as np
import os
import ray
import time
ray.init(ignore_reinit_error=True)
class RandomError(Exception):
pass
@ray.remote(max_retries=1, retry_exceptions=True)
def potentially_fail(failure_probability):
if failure_probability < 0 or failure_probability > 1:
raise ValueError(
"failure_probability must be between 0 and 1, but got: "
f"{failure_probability}"
)
time.sleep(0.2)
if np.random.random() < failure_probability:
raise RandomError("Failed!")
return 0
for _ in range(3):
try:
# If this task crashes, Ray will retry it up to one additional
# time. If either of the attempts succeeds, the call to ray.get
# below will return normally. Otherwise, it will raise an
# exception.
ray.get(potentially_fail.remote(0.5))
print('SUCCESS')
except RandomError:
print('FAILURE')
# Provide the exceptions that we want to retry as an allowlist.
retry_on_exception = potentially_fail.options(retry_exceptions=[RandomError])
try:
# This will fail since we're passing in -1 for the failure_probability,
# which will raise a ValueError in the task and does not match the RandomError
# exception that we provided.
ray.get(retry_on_exception.remote(-1))
except ValueError:
print("FAILED AS EXPECTED")
else:
raise RuntimeError("An exception should be raised so this shouldn't be reached.")
# These will retry on the RandomError exception.
for _ in range(3):
try:
# If this task crashes, Ray will retry it up to one additional
# time. If either of the attempts succeeds, the call to ray.get
# below will return normally. Otherwise, it will raise an
# exception.
ray.get(retry_on_exception.remote(0.5))
print('SUCCESS')
except RandomError:
print('FAILURE AFTER RETRIES')
# __tasks_fault_tolerance_retries_exception_end__
# fmt: on
+14
View File
@@ -0,0 +1,14 @@
import time
import ray
# Instead of "from tqdm import tqdm", use:
from ray.experimental.tqdm_ray import tqdm
@ray.remote
def f(name):
for x in tqdm(range(100), desc=name):
time.sleep(0.1)
ray.get([f.remote("task 1"), f.remote("task 2")])
+40
View File
@@ -0,0 +1,40 @@
load("//bazel:python.bzl", "py_test_run_all_notebooks")
filegroup(
name = "core_examples",
srcs = glob(["*.ipynb"]),
visibility = ["//doc:__subpackages__"]
)
# --------------------------------------------------------------------
# Test the lightweight, CPU-only Ray Core example notebooks. The
# excluded notebooks need GPU, long-running training, or live network
# access and aren't runnable in the standard CPU CI job.
# --------------------------------------------------------------------
py_test_run_all_notebooks(
size = "large",
include = ["*.ipynb"],
exclude = [
"batch_prediction.ipynb", # Requires GPU (num_gpus=1) and torch.
"highly_parallel.ipynb", # Needs an existing multi-node cluster and belongs in a release test.
"plot_hyperparameter.ipynb", # Model-training loop.
"plot_parameter_server.ipynb", # Torch training loop; slow/non-deterministic.
"plot_pong_example.ipynb", # Long-running RL (Pong) training.
"web_crawler.ipynb", # Makes live network requests.
],
data = ["//doc/source/ray-core/examples:core_examples"],
tags = [
"exclusive",
"team:core",
],
)
filegroup(
name = "core_examples_ci_configs",
srcs = glob([
"**/ci/aws.yaml",
"**/ci/gce.yaml",
]),
visibility = ["//doc:__pkg__"],
)
@@ -0,0 +1,345 @@
{
"cells": [
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"# Batch Prediction with Ray Core\n",
"\n",
"<a id=\"try-anyscale-quickstart-batch_prediction\" href=\"https://console.anyscale.com/register/ha?render_flow=ray&utm_source=ray_docs&utm_medium=docs&utm_campaign=batch_prediction\">\n",
" <img src=\"../../_static/img/run-on-anyscale.svg\" alt=\"try-anyscale-quickstart\">\n",
"</a>\n",
"<br></br>\n",
"\n",
"```{note}\n",
"For a higher level API for batch inference on large datasets, see [batch inference with Ray Data](batch_inference_home). This example is for users who want more control over data sharding and execution.\n",
"```\n",
"\n",
"The batch prediction is the process of using a trained model to generate predictions for a collection of observations. It has the following elements:\n",
"* Input dataset: this is a collection of observations to generate predictions for. The data is usually stored in an external storage system like S3, HDFS or database, and can be large.\n",
"* ML model: this is a trained ML model which is usually also stored in an external storage system.\n",
"* Predictions: these are the outputs when applying the ML model on observations. The predictions are usually written back to the storage system.\n",
"\n",
"With Ray, you can build scalable batch prediction for large datasets at high prediction throughput. Ray Data provides a [higher-level API for offline batch inference](batch_inference_home), with built-in optimizations. However, for more control, you can use the lower-level Ray Core APIs. This example demonstrates batch inference with Ray Core by splitting the dataset into disjoint shards and executing them in parallel, with either Ray Tasks or Ray Actors across a Ray Cluster."
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"## Task-based batch prediction\n",
"\n",
"With Ray tasks, you can build a batch prediction program in this way:\n",
"1. Loads the model\n",
"2. Launches Ray tasks, with each taking in the model and a shard of input dataset\n",
"3. Each worker executes predictions on the assigned shard, and writes out results\n",
"\n",
"Lets take NYC taxi data in 2009 for example. Suppose we have this simple model:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"\n",
"def load_model():\n",
" # A dummy model.\n",
" def model(batch: pd.DataFrame) -> pd.DataFrame:\n",
" # Dummy payload so copying the model will actually copy some data\n",
" # across nodes.\n",
" model.payload = np.zeros(100_000_000)\n",
" return pd.DataFrame({\"score\": batch[\"passenger_count\"] % 2 == 0})\n",
" \n",
" return model"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The dataset has 12 files (one for each month) so we can naturally have each Ray task to take one file. By taking the model and a shard of input dataset (i.e. a single file), we can define a Ray remote task for prediction:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import pyarrow.parquet as pq\n",
"import ray\n",
"\n",
"@ray.remote\n",
"def make_prediction(model, shard_path):\n",
" df = pq.read_table(shard_path).to_pandas()\n",
" result = model(df)\n",
"\n",
" # Write out the prediction result.\n",
" # NOTE: unless the driver will have to further process the\n",
" # result (other than simply writing out to storage system),\n",
" # writing out at remote task is recommended, as it can avoid\n",
" # congesting or overloading the driver.\n",
" # ...\n",
"\n",
" # Here we just return the size about the result in this example.\n",
" return len(result)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The driver launches all tasks for the entire input dataset. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 12 files, one for each remote task.\n",
"input_files = [\n",
" f\"s3://anonymous@air-example-data/ursa-labs-taxi-data/downsampled_2009_full_year_data.parquet\"\n",
" f\"/fe41422b01c04169af2a65a83b753e0f_{i:06d}.parquet\"\n",
" for i in range(12)\n",
"]\n",
"\n",
"# ray.put() the model just once to local object store, and then pass the\n",
"# reference to the remote tasks.\n",
"model = load_model()\n",
"model_ref = ray.put(model)\n",
"\n",
"result_refs = []\n",
"\n",
"# Launch all prediction tasks.\n",
"for file in input_files:\n",
" # Launch a prediction task by passing model reference and shard file to it.\n",
" # NOTE: it would be highly inefficient if you are passing the model itself\n",
" # like make_prediction.remote(model, file), which in order to pass the model\n",
" # to remote node will ray.put(model) for each task, potentially overwhelming\n",
" # the local object store and causing out-of-disk error.\n",
" result_refs.append(make_prediction.remote(model_ref, file))\n",
"\n",
"results = ray.get(result_refs)\n",
"\n",
"# Let's check prediction output size.\n",
"for r in results:\n",
" print(\"Prediction output size:\", r)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In order to not overload the cluster and cause OOM, we can control the parallelism by setting the proper resource requirement for tasks, see details about this design pattern in {doc}`/ray-core/patterns/limit-running-tasks`.\n",
"For example, if it's easy for your to get a good estimate of the in-memory size for data loaded from external storage, you can control the parallelism by specifying the amount of memory needed for each task, e.g. launching tasks with ``make_prediction.options(memory=100*1023*1025).remote(model_ref, file)``. Ray will then do the right thing and make sure tasks scheduled to a node will not exceed its total memory."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```{tip}\n",
"To avoid repeatedly storing the same model into object store (this can cause Out-of-disk for driver node), use ray.put() to store the model once, and then pass the reference around.\n",
"```\n",
"```{tip}\n",
"To avoid congest or overload the driver node, its preferable to have each task to write out the predictions (instead of returning results back to driver which actualy does nothing but write out to storage system).\n",
"```\n",
"\n",
"## Actor-based batch prediction\n",
"In the above solution, each Ray task will have to fetch the model from the driver node before it can start performing the prediction. This is an overhead cost that can be significant if the model size is large. We can optimize it by using Ray actors, which will fetch the model just once and reuse it for all tasks assigned to the actor.\n",
"\n",
"First, we define a callable class thats structured with an interface (i.e. constructor) to load/cache the model, and the other to take in a file and perform prediction."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import pyarrow.parquet as pq\n",
"import ray\n",
"\n",
"@ray.remote\n",
"class BatchPredictor:\n",
" def __init__(self, model):\n",
" self.model = model\n",
" \n",
" def predict(self, shard_path):\n",
" df = pq.read_table(shard_path).to_pandas()\n",
" result =self.model(df)\n",
"\n",
" # Write out the prediction result.\n",
" # NOTE: unless the driver will have to further process the\n",
" # result (other than simply writing out to storage system),\n",
" # writing out at remote task is recommended, as it can avoid\n",
" # congesting or overloading the driver.\n",
" # ...\n",
"\n",
" # Here we just return the size about the result in this example.\n",
" return len(result)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The constructor is called only once per actor worker. We use ActorPool to manage a set of actors that can receive prediction requests."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from ray.util.actor_pool import ActorPool\n",
"\n",
"model = load_model()\n",
"model_ref = ray.put(model)\n",
"num_actors = 4\n",
"actors = [BatchPredictor.remote(model_ref) for _ in range(num_actors)]\n",
"pool = ActorPool(actors)\n",
"input_files = [\n",
" f\"s3://anonymous@air-example-data/ursa-labs-taxi-data/downsampled_2009_full_year_data.parquet\"\n",
" f\"/fe41422b01c04169af2a65a83b753e0f_{i:06d}.parquet\"\n",
" for i in range(12)\n",
"]\n",
"for file in input_files:\n",
" pool.submit(lambda a, v: a.predict.remote(v), file)\n",
"while pool.has_next():\n",
" print(\"Prediction output size:\", pool.get_next())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note that the ActorPool is fixed in size, unlike task-based approach where the number of parallel tasks can be dynamic (as long as it's not exceeding max_in_flight_tasks). To have autoscaling actor pool, you will need to use the [Ray Data batch prediction](batch_inference_home)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Batch prediction with GPUs\n",
"\n",
"If your cluster has GPU nodes and your predictor can utilize the GPUs, you can direct the tasks or actors to those GPU nodes by specifying num_gpus. Ray will schedule them onto GPU nodes accordingly. On the node, you will need to move the model to GPU. The following is an example for Torch model."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"\n",
"@ray.remote(num_gpus=1)\n",
"def make_torch_prediction(model: torch.nn.Module, shard_path):\n",
" # Move model to GPU.\n",
" model.to(torch.device(\"cuda\"))\n",
" inputs = pq.read_table(shard_path).to_pandas().to_numpy()\n",
"\n",
" results = []\n",
" # for each tensor in inputs:\n",
" # results.append(model(tensor))\n",
" #\n",
" # Write out the results right in task instead of returning back\n",
" # to the driver node (unless you have to), to avoid congest/overload\n",
" # driver node.\n",
" # ...\n",
"\n",
" # Here we just return simple/light meta information.\n",
" return len(results)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## FAQs\n",
"\n",
"### How to load and pass model efficiently in Ray cluster if the model is large?\n",
"The recommended way is to (taking task-based batch prediction for example, the actor-based is the same):\n",
"1. let the driver load the model (e.g. from storage system)\n",
"2. let the driver ray.put(model) to store the model into object store; and\n",
"3. pass the same reference of the model to each remote tasks when launching them.\n",
"The remote task will fetch the model (from driver's object store) to its local object store before start performing prediction.\n",
"\n",
"Note it's highly inefficient if you skip the step 2 and pass the model (instead of reference) to remote tasks. If the model is large and there are many tasks, it'll likely cause out-of-disk crash for the driver node."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# GOOD: the model will be stored to driver's object store only once\n",
"model = load_model()\n",
"model_ref = ray.put(model)\n",
"for file in input_files:\n",
" make_prediction.remote(model_ref, file)\n",
"\n",
"# BAD: the same model will be stored to driver's object store repeatedly for each task\n",
"model = load_model()\n",
"for file in input_files:\n",
" make_prediction.remote(model, file)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For more details, check out {doc}`/ray-core/patterns/pass-large-arg-by-value`."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### How to improve the GPU utilization rate?\n",
"To keep GPUs busy, there are following things to look at:\n",
"- **Schedule multiple tasks on the same GPU node if it has multiple GPUs**: If there are multiple GPUs on same node and a single task cannot use them all, you can direct multiple tasks to the node. This is automatically handled by Ray, e.g. if you specify num_gpus=1 and there are 4 GPUs, Ray will schedule 4 tasks to the node, provided there are enough tasks and no other resource constraints.\n",
"- **Use actor-based approach**: as mentioned above, actor-based approach is more efficient because it reuses model initialization for many tasks, so the node will spend more time on the actual workload."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3.8.10 ('venv': venv)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.10"
},
"orig_nbformat": 4,
"vscode": {
"interpreter": {
"hash": "3c0d54d489a08ae47a06eae2fd00ff032d6cddb527c382959b7b2575f6a8167f"
}
}
},
"nbformat": 4,
"nbformat_minor": 2
}
@@ -0,0 +1,781 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "9cdddbae",
"metadata": {},
"source": [
"# A Gentle Introduction to Ray Core by Example\n",
"\n",
"<a id=\"try-anyscale-quickstart-gentle_walkthrough\" href=\"https://console.anyscale.com/register/ha?render_flow=ray&utm_source=ray_docs&utm_medium=docs&utm_campaign=gentle_walkthrough\">\n",
" <img src=\"../../_static/img/run-on-anyscale.svg\" alt=\"try-anyscale-quickstart\">\n",
"</a>\n",
"<br></br>"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "9d4d0ecd",
"metadata": {},
"source": [
"Implement a function in Ray Core to understand how Ray works and its basic concepts.\n",
"Python programmers from those with less experience to those who are interested in advanced tasks,\n",
"can start working with distributed computing using Python by learning the Ray Core API.\n",
"\n",
"## Install Ray\n",
"Install Ray with the following command:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6115afbb",
"metadata": {},
"outputs": [],
"source": [
"! pip install ray"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "be9d3c98",
"metadata": {},
"source": [
"## Ray Core\n",
"\n",
"Start a local cluster by running the following commands:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"import ray\n",
"ray.init()"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {
"collapsed": false
},
"source": [
"Note the following lines in the output:"
]
},
{
"cell_type": "markdown",
"id": "e75826f4",
"metadata": {},
"source": [
"```\n",
"... INFO services.py:1263 -- View the Ray dashboard at http://127.0.0.1:8265\n",
"{'node_ip_address': '192.168.1.41',\n",
"...\n",
"'node_id': '...'}\n",
"```"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "4c837608",
"metadata": {},
"source": [
"These messages indicate that the Ray cluster is working as expected. In this example output, the address of the Ray dashboard is `http://127.0.0.1:8265`. Access the Ray dashboard at the address on the first line of your output. The Ray dashboard displays information such as the number of CPU cores available and the total utilization of the current Ray application.\n",
"This is a typical output for a laptop:\n",
"\n",
"```\n",
"{'CPU': 12.0,\n",
"'memory': 14203886388.0,\n",
"'node:127.0.0.1': 1.0,\n",
"'object_store_memory': 2147483648.0}\n",
"```"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "cf542ed9",
"metadata": {},
"source": [
"Next, is a brief introduction to the Ray Core API, which we refer to as the Ray API.\n",
"The Ray API builds on concepts such as decorators, functions, and classes, that are familiar to Python programmers.\n",
"It is a universal programming interface for distributed computing. \n",
"The engine handles the complicated work, allowing developers to use Ray with existing Python libraries and systems."
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "83d1a3bf",
"metadata": {},
"source": [
"## Your First Ray API Example\n",
"\n",
"The following function retrieves and processes\n",
"data from a database. The dummy `database` is a plain Python list containing the\n",
"words of the title of the [\"Learning Ray\" book](https://www.amazon.com/Learning-Ray-Flexible-Distributed-Machine/dp/1098117220/).\n",
"The `sleep` function pauses for a certain amount of time to simulate the cost of accessing and processing data from the database. "
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "e053331e",
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"\n",
"database = [\n",
" \"Learning\", \"Ray\",\n",
" \"Flexible\", \"Distributed\", \"Python\", \"for\", \"Machine\", \"Learning\"\n",
"]\n",
"\n",
"\n",
"def retrieve(item):\n",
" time.sleep(item / 10.)\n",
" return item, database[item]"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "2b518f4f",
"metadata": {},
"source": [
"If the item with index 5 takes half a second `(5 / 10.)`, an estimate of the total runtime to retrieve all eight items sequentially is `(0+1+2+3+4+5+6+7)/10. = 2.8` seconds.\n",
"Run the following code to get the actual time:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "b0091149",
"metadata": {
"lines_to_next_cell": 2
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Runtime: 2.82 seconds, data:\n",
"(0, 'Learning')\n",
"(1, 'Ray')\n",
"(2, 'Flexible')\n",
"(3, 'Distributed')\n",
"(4, 'Python')\n",
"(5, 'for')\n",
"(6, 'Machine')\n",
"(7, 'Learning')\n"
]
}
],
"source": [
"def print_runtime(input_data, start_time):\n",
" print(f'Runtime: {time.time() - start_time:.2f} seconds, data:')\n",
" print(*input_data, sep=\"\\n\")\n",
"\n",
"\n",
"start = time.time()\n",
"data = [retrieve(item) for item in range(8)]\n",
"print_runtime(data, start)"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "97aa047d",
"metadata": {},
"source": [
"The total time to run the function is 2.82 seconds in this example, but time may be different for your computer.\n",
"Note that this basic Python version cannot run the function simultaneously."
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "30291db3",
"metadata": {},
"source": [
"You may expect that Python list comprehensions are more efficient. The measured runtime of 2.8 seconds is actually the worst case scenario.\n",
"Although this program \"sleeps\" for most of its runtime, it is slow because of the Global Interpreter Lock (GIL)."
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "2ebb32d4",
"metadata": {},
"source": [
"### Ray Tasks\n",
"\n",
"This task can benefit from parallelization. If it is perfectly distributed, the runtime should not take much longer than the slowest subtask,\n",
"that is, `7/10. = 0.7` seconds.\n",
"To extend this example to run in parallel on Ray, start by using the @ray.remote decorator:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "1e21e7c7",
"metadata": {},
"outputs": [],
"source": [
"import ray \n",
"\n",
"\n",
"@ray.remote\n",
"def retrieve_task(item):\n",
" return retrieve(item)"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "935a4062",
"metadata": {},
"source": [
"With the decorator, the function `retrieve_task` becomes a :ref:`ray-remote-functions<Ray task>`_.\n",
"A Ray task is a function that Ray executes on a different process from where\n",
"it was called, and possibly on a different machine."
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "78afc80a",
"metadata": {},
"source": [
"Ray is convenient to use because you can continue writing Python code,\n",
"without having to significantly change your approach or programming style.\n",
"Using the :func:`ray.remote()<@ray.remote>` decorator on the retrieve function is the intended use of decorators,\n",
"and you did not modify the original code in this example."
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "c5dc2e18",
"metadata": {},
"source": [
"To retrieve database entries and measure performance, you do not need to make many changes to the code. Here's an overview of the process:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "a34697da",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"2022-12-20 13:52:34,632\tINFO worker.py:1529 -- Started a local Ray instance. View the dashboard at \u001b[1m\u001b[32m127.0.0.1:8265 \u001b[39m\u001b[22m\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Runtime: 0.71 seconds, data:\n",
"(0, 'Learning')\n",
"(1, 'Ray')\n",
"(2, 'Flexible')\n",
"(3, 'Distributed')\n",
"(4, 'Python')\n",
"(5, 'for')\n",
"(6, 'Machine')\n",
"(7, 'Learning')\n"
]
}
],
"source": [
"start = time.time()\n",
"object_references = [\n",
" retrieve_task.remote(item) for item in range(8)\n",
"]\n",
"data = ray.get(object_references)\n",
"print_runtime(data, start)"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "8df4087c",
"metadata": {},
"source": [
"Running the task in parallel requires two minor code modifications.\n",
"To execute your Ray task remotely, you must use a `.remote()` call.\n",
"Ray executes remote tasks asynchronously, even on a local cluster.\n",
"The items in the `object_references` list in the code snippet do not directly contain the results.\n",
"If you check the Python type of the first item using `type(object_references[0])`,\n",
"you see that it is actually an `ObjectRef`.\n",
"These object references correspond to _futures_ for which you need to request the result.\n",
"The call :func:`ray.get()<ray.get(...)>` is for requesting the result. Whenever you call remote on a Ray task,\n",
"it immediately returns one or more object references.\n",
"Consider Ray tasks as the primary way of creating objects.\n",
"The following section is an example that links multiple tasks together and allows\n",
"Ray to pass and resolve the objects between them."
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "2373ddd9",
"metadata": {},
"source": [
"Let's review the previous steps.\n",
"You started with a Python function, then decorated it with `@ray.remote`, making the function a Ray task.\n",
"Instead of directly calling the original function in the code, you called `.remote(...)` on the Ray task.\n",
"Finally, you retrieved the results from the Ray cluster using `.get(...)`.\n",
"Consider creating a Ray task from one of your own functions as an additional exercise."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": []
},
{
"attachments": {},
"cell_type": "markdown",
"id": "e008a500",
"metadata": {},
"source": [
"Let's review the performance gain from using Ray tasks.\n",
"On most laptops the runtime is around 0.71 seconds,\n",
"which is slightly more than the slowest subtask, which is 0.7 seconds.\n",
"You can further improve the program by leveraging more of Rays API."
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "54f53644",
"metadata": {},
"source": [
"### Object Stores\n",
"\n",
"The retrieve definition directly accesses items from the `database`. While this works well on a local Ray cluster, consider how it functions on an actual cluster with multiple computers.\n",
"A Ray cluster has a head node with a driver process and multiple worker nodes with worker processes executing tasks.\n",
"In this scenario the database is only defined on the driver, but the worker processes need access to it to run the retrieve task.\n",
"Ray's solution for sharing objects between the driver and workers or between workers is to use\n",
"the `ray.put` function to place the data into Ray's distributed object store.\n",
"In the `retrieve_task` definition, you can add a `db` argument to pass later as the `db_object_ref` object."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "da66a836",
"metadata": {},
"outputs": [],
"source": [
"db_object_ref = ray.put(database)\n",
"\n",
"\n",
"@ray.remote\n",
"def retrieve_task(item, db):\n",
" time.sleep(item / 10.)\n",
" return item, db[item]"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "72f37eb4",
"metadata": {},
"source": [
"By using the object store, you allow Ray to manage data access throughout the entire cluster.\n",
"Although the object store involves some overhead, it improves performance for larger datasets.\n",
"This step is crucial for a truly distributed environment.\n",
"Rerun the example with the `retrieve_task` function to confirm that it executes as you expect."
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "453e312f",
"metadata": {},
"source": [
"### Non-blocking calls\n",
"\n",
"In the previous section, you used `ray.get(object_references)` to retrieve results.\n",
"This call blocks the driver process until all results are available.\n",
"This dependency can cause problems if each database item takes several minutes to process.\n",
"More efficiency gains are possible if you allow the driver process to perform other tasks while waiting for results,\n",
"and to process results as they are completed rather than waiting for all items to finish.\n",
"Additionally, if one of the database items cannot be retrieved due to an issue like a deadlock in the database connection,\n",
"the driver hangs indefinitely.\n",
"To prevent indefinite hangs, set reasonable `timeout` values when using the `wait` function.\n",
"For example, if you want to wait less than ten times the time of the slowest data retrieval task,\n",
"use the `wait` function to stop the task after that time has passed."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "75da06ec",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Runtime: 0.11 seconds, data:\n",
"(0, 'Learning')\n",
"(1, 'Ray')\n",
"Runtime: 0.31 seconds, data:\n",
"(2, 'Flexible')\n",
"(3, 'Distributed')\n",
"Runtime: 0.51 seconds, data:\n",
"(4, 'Python')\n",
"(5, 'for')\n",
"Runtime: 0.71 seconds, data:\n",
"(6, 'Machine')\n",
"(7, 'Learning')\n",
"Runtime: 0.71 seconds, data:\n",
"(0, 'Learning')\n",
"(1, 'Ray')\n",
"(2, 'Flexible')\n",
"(3, 'Distributed')\n",
"(4, 'Python')\n",
"(5, 'for')\n",
"(6, 'Machine')\n",
"(7, 'Learning')\n"
]
}
],
"source": [
"start = time.time()\n",
"object_references = [\n",
" retrieve_task.remote(item, db_object_ref) for item in range(8)\n",
"]\n",
"all_data = []\n",
"\n",
"while len(object_references) > 0:\n",
" finished, object_references = ray.wait(\n",
" object_references, timeout=7.0\n",
" )\n",
" data = ray.get(finished)\n",
" print_runtime(data, start)\n",
" all_data.extend(data)\n",
"\n",
"print_runtime(all_data, start)"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "cf6f00c3",
"metadata": {},
"source": [
"Instead of printing the results, you can use the retrieved values\n",
"within the `while` loop to initiate new tasks on other workers."
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "1a9f6be5",
"metadata": {},
"source": [
"### Task dependencies\n",
"\n",
"You may want to perform an additional processing task on the retrieved data. For example, \n",
"use the results from the first retrieval task to query other related data from the same database (perhaps from a different table).\n",
"The code below sets up this follow-up task and executes both the `retrieve_task` and `follow_up_task` in sequence."
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "f5734bb1",
"metadata": {
"lines_to_next_cell": 2
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"((0, 'Learning'), (1, 'Ray'))\n",
"((2, 'Flexible'), (3, 'Distributed'))\n",
"((4, 'Python'), (5, 'for'))\n",
"((6, 'Machine'), (7, 'Learning'))\n"
]
}
],
"source": [
"@ray.remote\n",
"def follow_up_task(retrieve_result):\n",
" original_item, _ = retrieve_result\n",
" follow_up_result = retrieve(original_item + 1)\n",
" return retrieve_result, follow_up_result\n",
"\n",
"\n",
"retrieve_refs = [retrieve_task.remote(item, db_object_ref) for item in [0, 2, 4, 6]]\n",
"follow_up_refs = [follow_up_task.remote(ref) for ref in retrieve_refs]\n",
"\n",
"result = [print(data) for data in ray.get(follow_up_refs)]"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "6e4a6945",
"metadata": {},
"source": [
"If you're unfamiliar with asynchronous programming, this example may not be particularly impressive.\n",
"However, at second glance it might be surprising that the code runs at all.\n",
"The code appears to be a regular Python function with a few list comprehensions.\n",
"\n",
"The function body of `follow_up_task` expects a Python tuple for its input argument `retrieve_result`.\n",
"However, when you use the `[follow_up_task.remote(ref) for ref in retrieve_refs]` command,\n",
"you are not passing tuples to the follow-up task.\n",
"Instead, you are using the `retrieve_refs` to pass in Ray object references.\n",
"\n",
"Behind the scenes, Ray recognizes that the `follow_up_task` needs actual values,\n",
"so it _automatically_ uses the `ray.get` function to resolve these futures.\n",
"Additionally, Ray creates a dependency graph for all the tasks and executes them in a way that respects their dependencies.\n",
"You don't have to explicitly tell Ray when to wait for a previous task to be completed––it infers the order of execution.\n",
"This feature of the Ray object store is useful because you avoid copying large intermediate values\n",
"back to the driver by passing the object references to the next task and letting Ray handle the rest."
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "f8722673",
"metadata": {},
"source": [
"The next steps in the process are only scheduled once the tasks specifically designed to retrieve information are completed.\n",
"In fact, if `retrieve_refs` was called `retrieve_result`, you might not have noticed this crucial and intentional naming nuance. Ray allows you to concentrate on your work rather than the technicalities of cluster computing.\n",
"The dependency graph for the two tasks looks like this:"
]
},
{
"cell_type": "markdown",
"id": "d03b8e46",
"metadata": {
"lines_to_next_cell": 2
},
"source": [
"![Task dependency](https://raw.githubusercontent.com/maxpumperla/learning_ray/main/notebooks/images/chapter_02/task_dependency.png)"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "e4001edb",
"metadata": {},
"source": [
"### Ray Actors\n",
"\n",
"This example covers one more significant aspect of Ray Core.\n",
"Up until this step, everything is essentially a function.\n",
"You used the `@ray.remote` decorator to make certain functions remote, but aside from that, you only used standard Python."
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "2a7a6e69",
"metadata": {},
"source": [
"If you want to keep track of how often the database is being queried, you could count the results of the retrieve tasks.\n",
"However, is there a more efficient way to do this? Ideally, you want to track this in a distributed manner that can handle a large amount of data.\n",
"Ray provides a solution with actors, which run stateful computations on a cluster and can also communicate with each other.\n",
"Similar to how you create Ray tasks using decorated functions, create Ray actors using decorated Python classes.\n",
"Therefore, you can create a simple counter using a Ray actor to track the number of database calls."
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "717df7d0",
"metadata": {},
"outputs": [],
"source": [
"@ray.remote\n",
"class DataTracker:\n",
" def __init__(self):\n",
" self._counts = 0\n",
"\n",
" def increment(self):\n",
" self._counts += 1\n",
"\n",
" def counts(self):\n",
" return self._counts"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "2e003cc6",
"metadata": {},
"source": [
"The DataTracker class becomes an actor when you give it the `ray.remote` decorator. This actor is capable of tracking state,\n",
"such as a count, and its methods are Ray actor tasks that you can invoke in the same way as functions using `.remote()`.\n",
"Modify the retrieve_task to incorporate this actor."
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "6843b8d9",
"metadata": {
"lines_to_next_cell": 2
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[(0, 'Learning'), (1, 'Ray'), (2, 'Flexible'), (3, 'Distributed'), (4, 'Python'), (5, 'for'), (6, 'Machine'), (7, 'Learning')]\n",
"8\n"
]
}
],
"source": [
"@ray.remote\n",
"def retrieve_tracker_task(item, tracker, db):\n",
" time.sleep(item / 10.)\n",
" tracker.increment.remote()\n",
" return item, db[item]\n",
"\n",
"\n",
"tracker = DataTracker.remote()\n",
"\n",
"object_references = [\n",
" retrieve_tracker_task.remote(item, tracker, db_object_ref) for item in range(8)\n",
"]\n",
"data = ray.get(object_references)\n",
"\n",
"print(data)\n",
"print(ray.get(tracker.counts.remote()))"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "ae886162",
"metadata": {},
"source": [
"As expected, the outcome of this calculation is 8.\n",
"Although you don't need actors to perform this calculation, this demonstrates a way to maintain state across the cluster, possibly involving multiple tasks.\n",
"In fact, you could pass the actor into any related task or even into the constructor of a different actor.\n",
"The Ray API is flexible, allowing for limitless possibilities.\n",
"It's rare for distributed Python tools to allow for stateful computations,\n",
"which is especially useful for running complex distributed algorithms such as reinforcement learning."
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "fb8bd0f5",
"metadata": {},
"source": [
"## Summary\n",
"\n",
"In this example, you only used six API methods.\n",
"These included `ray.init()` to initiate the cluster, `@ray.remote` to transform functions and classes into tasks and actors,\n",
"`ray.put()` to transfer values into Ray's object store, and `ray.get()` to retrieve objects from the cluster.\n",
"Additionally, you used `.remote()` on actor methods or tasks to execute code on the cluster, and `ray.wait` to prevent blocking calls."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0d936caa",
"metadata": {},
"outputs": [],
"source": []
},
{
"attachments": {},
"cell_type": "markdown",
"id": "0d8693e9",
"metadata": {},
"source": [
"The Ray API consists of more than these six calls, but these six are powerful, if you're just starting out.\n",
"To summarize more generally, the methods are as follows:"
]
},
{
"cell_type": "markdown",
"id": "f2fb14bd",
"metadata": {},
"source": [
"- `ray.init()`: Initializes your Ray cluster. Pass in an address to connect to an existing cluster.\n",
"- `@ray.remote`: Turns functions into tasks and classes into actors.\n",
"- `ray.put()`: Puts values into Rays object store.\n",
"- `ray.get()`: Gets values from the object store. Returns the values youve put there or that were computed by a task or actor.\n",
"- `.remote()`: Runs actor methods or tasks on your Ray cluster and is used to instantiate actors.\n",
"- `ray.wait()`: Returns two lists of object references, one with finished tasks were waiting for and one with unfinished tasks."
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": false
},
"source": [
"## Want to learn more?\n",
"\n",
"This example is a simplified version of the Ray Core walkthrough of [our \"Learning Ray\" book](https://maxpumperla.com/learning_ray/).\n",
"If you liked it, check out the [Ray Core Examples Gallery](./overview.rst) or some of the ML workloads in our [Use Case Gallery](../../ray-overview/use-cases.rst)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": []
}
],
"metadata": {
"jupytext": {
"cell_metadata_filter": "-all",
"main_language": "python",
"notebook_metadata_filter": "-all"
},
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,338 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "515dffba",
"metadata": {},
"source": [
"# Using Ray for Highly Parallelizable Tasks\n",
"\n",
"<a id=\"try-anyscale-quickstart-highly_parallel\" href=\"https://console.anyscale.com/register/ha?render_flow=ray&utm_source=ray_docs&utm_medium=docs&utm_campaign=highly_parallel\">\n",
" <img src=\"../../_static/img/run-on-anyscale.svg\" alt=\"try-anyscale-quickstart\">\n",
"</a>\n",
"<br></br>\n",
"\n",
"While Ray can be used for very complex parallelization tasks,\n",
"often we just want to do something simple in parallel.\n",
"For example, we may have 100,000 time series to process with exactly the same algorithm,\n",
"and each one takes a minute of processing.\n",
"\n",
"Clearly running it on a single processor is prohibitive: this would take 70 days.\n",
"Even if we managed to use 8 processors on a single machine,\n",
"that would bring it down to 9 days. But if we can use 8 machines, each with 16 cores,\n",
"it can be done in about 12 hours.\n",
"\n",
"How can we use Ray for these types of task? \n",
"\n",
"We take the simple example of computing the digits of pi.\n",
"The algorithm is simple: generate random x and y, and if ``x^2 + y^2 < 1``, it's\n",
"inside the circle, we count as in. This actually turns out to be pi/4\n",
"(remembering your high school math).\n",
"\n",
"The following code (and this notebook) assumes you have already set up your Ray cluster and that you are running on the head node. For more details on how to set up a Ray cluster please see [Ray Clusters Getting Started](https://docs.ray.io/en/master/cluster/getting-started.html). \n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "8e3e7c4f",
"metadata": {},
"outputs": [],
"source": [
"import ray\n",
"import random\n",
"import time\n",
"import math\n",
"from fractions import Fraction"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "92d2461b",
"metadata": {
"scrolled": true,
"tags": [
"remove-output"
]
},
"outputs": [],
"source": [
"# Let's start Ray\n",
"ray.init(address='auto')"
]
},
{
"cell_type": "markdown",
"id": "b96f2eb9",
"metadata": {},
"source": [
"We use the ``@ray.remote`` decorator to create a Ray task.\n",
"A task is like a function, except the result is returned asynchronously.\n",
"\n",
"It also may not run on the local machine, it may run elsewhere in the cluster.\n",
"This way you can run multiple tasks in parallel,\n",
"beyond the limit of the number of processors you can have in a single machine."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "ece9887c",
"metadata": {},
"outputs": [],
"source": [
"@ray.remote\n",
"def pi4_sample(sample_count):\n",
" \"\"\"pi4_sample runs sample_count experiments, and returns the \n",
" fraction of time it was inside the circle. \n",
" \"\"\"\n",
" in_count = 0\n",
" for i in range(sample_count):\n",
" x = random.random()\n",
" y = random.random()\n",
" if x*x + y*y <= 1:\n",
" in_count += 1\n",
" return Fraction(in_count, sample_count)\n"
]
},
{
"cell_type": "markdown",
"id": "05bf8675",
"metadata": {},
"source": [
"To get the result of a future, we use ray.get() which \n",
"blocks until the result is complete. "
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "9d9a3509",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Running 1000000 tests took 1.4935967922210693 seconds\n"
]
}
],
"source": [
"SAMPLE_COUNT = 1000 * 1000\n",
"start = time.time() \n",
"future = pi4_sample.remote(sample_count = SAMPLE_COUNT)\n",
"pi4 = ray.get(future)\n",
"end = time.time()\n",
"dur = end - start\n",
"print(f'Running {SAMPLE_COUNT} tests took {dur} seconds')"
]
},
{
"cell_type": "markdown",
"id": "cc17429d",
"metadata": {},
"source": [
"Now let's see how good our approximation is."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "42d4c464",
"metadata": {},
"outputs": [],
"source": [
"pi = pi4 * 4"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "4009bee0",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"3.143024"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"float(pi)"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "d19155d6",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"0.0004554042254233261"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"abs(pi-math.pi)/pi"
]
},
{
"cell_type": "markdown",
"id": "ddb3b095",
"metadata": {},
"source": [
"Meh. A little off -- that's barely 4 decimal places.\n",
"Why don't we do it a 100,000 times as much? Let's do 100 billion!"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "b7b9cff9",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Doing 100000 batches\n"
]
}
],
"source": [
"FULL_SAMPLE_COUNT = 100 * 1000 * 1000 * 1000 # 100 billion samples! \n",
"BATCHES = int(FULL_SAMPLE_COUNT / SAMPLE_COUNT)\n",
"print(f'Doing {BATCHES} batches')\n",
"results = []\n",
"for _ in range(BATCHES):\n",
" results.append(pi4_sample.remote(sample_count = SAMPLE_COUNT))\n",
"output = ray.get(results)"
]
},
{
"cell_type": "markdown",
"id": "94264de4",
"metadata": {},
"source": [
"Notice that in the above, we generated a list with 100,000 futures.\n",
"Now all we do is have to do is wait for the result.\n",
"\n",
"Depending on your ray cluster's size, this might take a few minutes.\n",
"But to give you some idea, if we were to do it on a single machine,\n",
"when I ran this it took 0.4 seconds.\n",
"\n",
"On a single core, that means we're looking at 0.4 * 100000 = about 11 hours. \n",
"\n",
"Here's what the Dashboard looks like: \n",
"\n",
"![View of the dashboard](../images/dashboard.png)\n",
"\n",
"So now, rather than just a single core working on this,\n",
"I have 168 working on the task together. And its ~80% efficient."
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "76eba02d",
"metadata": {},
"outputs": [],
"source": [
"pi = sum(output)*4/len(output)"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "ede2bd8c",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"3.14159518188"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"float(pi)"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "bb62cb27",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"8.047791203506436e-07"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"abs(pi-math.pi)/pi"
]
},
{
"cell_type": "markdown",
"id": "30d12e50",
"metadata": {},
"source": [
"Not bad at all -- we're off by a millionth. "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1b36747b",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"celltoolbar": "Tags",
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.7 KiB

Some files were not shown because too many files have changed in this diff Show More