Skip to content

[DO NOT MERGE][core] Fix actor arg fetch pipelining v2#63765

Open
karticam wants to merge 11 commits into
ray-project:masterfrom
karticam:karticam/fix-actor-arg-fetch-pipelining-v2
Open

[DO NOT MERGE][core] Fix actor arg fetch pipelining v2#63765
karticam wants to merge 11 commits into
ray-project:masterfrom
karticam:karticam/fix-actor-arg-fetch-pipelining-v2

Conversation

@karticam

@karticam karticam commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Issue
Actor task arg-fetching was getting blocked by in-flight actor task execution. When a PushTask RPC arrives, the worker sends an IPC to the raylet asking it to pull the args; that IPC was being dispatched on task_execution_service_, the same loop that runs user actor task bodies. If the actor was busy executing a task, the IPC sat in the queue behind it — defeating the whole point of pipelining args-fetch with execution.

Fix
Restructure so that only the user task body (request.Execute()) runs on task_execution_service_ (or a concurrency-group pool / asyncio fiber). Everything else — queue bookkeeping, args-fetch IPC dispatch, cancellation decisions, dependency resolution, timer management — runs on io_service_

Changes:

  • OrderedActorTaskExecutionQueue and UnorderedActorTaskExecutionQueue now take both io_service_ and
    task_execution_service_. The wait_timer_ and all internal state live on io_service_.
  • TaskReceiver::QueueTaskForExecution for actor tasks is no longer wrapped in task_execution_service_.post(...) — it runs inline on io_service_ from HandlePushTask.
  • CoreWorker::HandleActorCallArgWaitComplete invokes MarkReady inline on io_service_ instead of posting to task_execution_service_.
  • CoreWorker::CancelActorTaskOnExecutor async-actor branch posts to io_service_ (where the queue state now lives) instead of task_execution_service_; the post is now a self-post and can be done inline.
  • CoreWorkerShutdownExecutor::ExecuteExit posts the drain/Stop chain to io_service_ instead of
    task_execution_service_.
  • Inside AcceptRequestOrRejectIfCanceled: the cancellation check runs on io_service_; only request.Execute() is posted off to the pool / fiber / task_execution_service_.

Notes:
Additionally I also found two potential bugs in existing code:

  1. Concurrent read/write on actor_task_execution_queues_ without a mutex.
    In master:
  • QueueTaskForExecution wrote the per-caller map (find+emplace) on task_execution_service_.
  • CancelQueuedActorTask for sync actors read the map on io_service_
    Accessing the hashmap from two threads is UB. After this change, both access paths are on io_service_.
  1. Accept-vs-Cancel
    In master, AcceptRequestOrRejectIfCanceled adn CancelTaskIfFound can run on different threads, Due to the release of mu_ before request.Execute(), we could have this race:
    a. Accept reads is_canceled=false, releases mu_.
    b. CancelTaskIfFound (concurrently) sets pending_task_id_to_is_canceled[task_id]=true, returns task_present=true.
    c. The cancel RPC reports success=true ("cancelled in queue") to the caller.
    d. Accept then calls request.Execute(). Task runs to completion anyway.

After this PR, both functions run on io_service_, and therefore are serialized.

Results:
I tested the change against a ray script which spins up a producer actor and a consumer actor on different nodes. The producer produces large objects which are consumed by the consumer triggering plasma object pulls. The consumer is slow (sleeps infinitely). Without the fix, the consumer pulls some objects, but as soon as it starts executing the task (which sleeps infinitely), object pulling stops and as a result, only some of the objects are pulled from the producer side. With the fix, all objects are pulled from producer.

Observe the object store memory column below (each object is 128MB)

image Producer produced 10 objects, consumer pulled only 3 (buggy case) image Producer produced 10 objects, consumer pulled all (fixed case)

TODO:
With the change in this PR, all submitted actor tasks will have their arguments prefetched. This might result in OOM issues or starvation of one actor tasks due to prefetching args of another actor tasks. To fix this, the solution would be to impose a limit on prefetches per concurrency group. I will take that up in a followup PR.

It is also important to note that the current design too doesn't protect against it intentionally. The current implementation also queues up arg fetches for all tasks until the time first arg fetch is complete and the task starts executing. In the worst case, it could also tigger prefetches enough to OOM if the first fetch takes a long time and the task is not scheduled.

@karticam karticam added core Issues that should be addressed in Ray Core go add ONLY when ready to merge, run all tests labels Jun 1, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the CoreWorker's actor task execution queues to separate queue bookkeeping (running on io_service_) from user task execution (running on task_execution_service_ or concurrency-group pools) to prevent long-running tasks from blocking gRPC handlers. Feedback on these changes highlights critical thread-safety and data race hazards in both OrderedActorTaskExecutionQueue and UnorderedActorTaskExecutionQueue due to concurrent queue state mutations across different threads in AcceptRequestOrRejectIfCanceled. Additionally, it is recommended to defensively post regular actor cancellations to io_service_ rather than executing them inline to avoid potential thread-safety issues.

Comment on lines +302 to +306
task_execution_service_.post(
[this, request = std::move(request), task_id]() mutable {
AcceptRequestOrRejectIfCanceled(task_id, request);
},
"OrderedActorTaskExecutionQueue.AcceptRequest");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Thread-Safety and Data Race Hazard

By posting AcceptRequestOrRejectIfCanceled to task_execution_service_ (or running it on a concurrency-group pool thread), the execution of the task and any subsequent queue bookkeeping/cleanup inside AcceptRequestOrRejectIfCanceled will run on a different thread than io_service_.

Since OrderedActorTaskExecutionQueue is not thread-safe and does not use internal locking, concurrent access to the queue's state (such as group_states_, pending_tasks_, or canceled_tasks_) from io_service_ (e.g., in EnqueueTask or CancelTaskIfFound) and task_execution_service_ / pool threads will result in data races and undefined behavior.

Furthermore, if there are any thread-affinity checks like RAY_CHECK(std::this_thread::get_id() == main_thread_id_) inside AcceptRequestOrRejectIfCanceled or functions it calls, they will fail and crash the worker because main_thread_id_ is now initialized to the io_service_ thread ID.

Recommendation:
Ensure that all queue state mutations and lookups are strictly serialized on io_service_, or introduce proper synchronization (e.g., a mutex) to protect the queue's internal state.

Comment on lines +165 to +169
task_execution_service_.post(
[this, request = std::move(request), task_id]() mutable {
AcceptRequestOrRejectIfCanceled(task_id, request);
},
"UnorderedActorTaskExecutionQueue.AcceptRequest");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Thread-Safety and Data Race Hazard

Similar to OrderedActorTaskExecutionQueue, posting AcceptRequestOrRejectIfCanceled to task_execution_service_ (or running it on a concurrency-group pool thread) means that the task execution and subsequent queue bookkeeping/cleanup will run on a different thread than io_service_.

Since UnorderedActorTaskExecutionQueue is not thread-safe and does not use internal locking, concurrent access to the queue's state (such as running_tasks_, pending_tasks_, or canceled_tasks_) from io_service_ (e.g., in EnqueueTask or CancelTaskIfFound) and task_execution_service_ / pool threads will result in data races and undefined behavior.

For example, in AcceptRequestOrRejectIfCanceled, when a deferred RunRequest is identified and posted back to io_service_, the queue's internal state is accessed/mutated on the task_execution_service_ / pool thread, which directly races with io_service_.

Recommendation:
Ensure that all queue state mutations and lookups are strictly serialized on io_service_, or introduce proper synchronization (e.g., a mutex) to protect the queue's internal state.

Comment thread src/ray/core_worker/core_worker.cc Outdated
Comment on lines 3989 to 3993
} else {
// For regular actor, we cannot post it to task_execution_service because
// main thread is blocked. Threaded actor can do both (dispatching to
// task execution service, or just directly call it in io_service).
// There's no special reason why we don't dispatch
// cancel to task_execution_service_ for threaded actors.
// For regular (sync) actor, we are already on io_service_ here (this is
// invoked from a gRPC handler), so call inline.
cancel();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Defensive Threading: Avoid Inline Execution on Assumed Thread

For regular (sync) actors, the code calls cancel() inline under the assumption that CancelActorTaskOnExecutor is always invoked on the io_service_ thread (e.g., from a gRPC handler).

However, if this method is ever called from a different thread (for example, during local cancellation, testing, or if the gRPC server thread pool configuration changes), calling cancel() inline will bypass the thread-safety guarantees of io_service_ and cause data races on the queue's bookkeeping state.

Recommendation:
To be defensive and robust against future refactorings or different invocation paths, consider posting the cancellation to io_service_ for regular actors as well, or add a thread-assertion before calling cancel() inline.

  } else {
    io_service_.post([cancel = std::move(cancel)]() { cancel(); },
                     "CoreWorker.CancelActorTaskOnExecutor");
  }

@karticam karticam changed the title [WIP][DO NOT MERGE] Fix actor arg fetch pipelining v2 [core] Fix actor arg fetch pipelining v2 Jun 3, 2026
@karticam
karticam marked this pull request as ready for review June 3, 2026 00:43
@karticam
karticam requested a review from a team as a code owner June 3, 2026 00:43

@stephanie-wang stephanie-wang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic looks good. I wonder if you can also add an integration test to check that actor arg fetch is being pipelined properly.

@karticam
karticam force-pushed the karticam/fix-actor-arg-fetch-pipelining-v2 branch from a1f7d89 to f7a1e74 Compare June 3, 2026 21:19
@karticam

karticam commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

The logic looks good. I wonder if you can also add an integration test to check that actor arg fetch is being pipelined properly.

Added the test in test_actor_advanced.py

Comment thread python/ray/tests/test_actor_advanced.py Outdated
Comment on lines +60 to +68
The IPC that tells the local raylet to pull a task's args runs on
io_service_. The user task body runs on task_execution_service_ (or a
concurrency-group pool). These are separate event loops, so while one
task body is executing, args for the next queued task can be pulled in
parallel.

Previously the IPC and the body shared task_execution_service_, so once
a body was running, IPCs for queued tasks could not fire and pulling was
delayed until the body completed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: these are core worker / C++ implementation details, they don't belong in a python integration test. there's also no need to describe the previous state of the code prior to the change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Comment thread python/ray/tests/test_actor_advanced.py Outdated
able to pull any args.

"""
from ray.experimental import get_object_locations

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for delayed import

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

assert set(node_ids) == worker_node_ids


def test_actor_arg_fetch_is_pipelined_with_task_execution(ray_start_cluster):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you run this w/o the code change and validate that it failed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. w/o code change, no args pulled and test times out

Comment on lines +81 to +82
// wait_timer_ is bound to io_service_ because the queue's bookkeeping
// runs on io_service_ now.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again no need to comment about the changes from previous impl like this (claude likes to do this)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cleaned up the comments

Comment on lines +337 to +342
if (pool == nullptr) {
task_execution_service_.post(std::move(execute_handler),
"OrderedActorTaskExecutionQueue.Execute");
} else {
pool->Post(std::move(execute_handler));
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice to put these behind some kind of shared interface so we don't need the branches

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made the change. LMK if this looks fine

Comment on lines +44 to +47
/// \param task_execution_service The io_context that user task bodies
/// (`request.Execute()`) are posted to when no concurrency-group thread pool
/// is available. Decoupled from `io_service` so a long-running user task
/// never blocks bookkeeping / arg-fetch IPCs on `io_service`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to my comment above, a cleaner way to structure this would be to have some kind of "postable" interface that both the thread pool and task execution service can implement. Then here you can pass in a "default task executor postable" (vague, hopefully you get the idea) and avoid exposing the details of "task_execution_service" here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +158 to +160
post_execute = [fiber = std::move(fiber)](std::function<void()> task) mutable {
fiber->EnqueueFiber(std::move(task));
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostExecuteFn looks a lot like what I suggested above :)

}

// Accept can be very long, and we shouldn't hold a lock.
// Helper: run the post-task-completion bookkeeping .

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't finish comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

};

if (is_canceled) {
// Note that cancel is now called on io_service_

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto again -- please spend time cleaning up the AI slop comments

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@karticam
karticam force-pushed the karticam/fix-actor-arg-fetch-pipelining-v2 branch from f7a1e74 to e46325d Compare June 13, 2026 08:47
"OrderedActorTaskExecutionQueue.Execute");
} else {
pool->Post(std::move(execute_handler));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cancel race after async execute post

Medium Severity

After AcceptRequestOrRejectIfCanceled sees is_canceled as false, it posts request.Execute() to another executor and returns on io_service_. A later CancelTaskIfFound on the same loop can set the cancel flag and report success while the already-posted handler still runs the task body, because execute_handler never re-reads the flag before calling Execute().

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e46325d. Configure here.

@edoakes

edoakes commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

There are a bunch of failing serve tests that I am guessing are related: https://buildkite.com/ray-project/premerge/builds/68205

Perhaps there are unintentional changes to asyncio actor behavior?

std::make_unique<OrderedActorTaskExecutionQueue>(
task_execution_service_,
io_service_,
std::move(default_postable),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale executors at queue creation

High Severity

Per-caller actor queues copy pool_manager_ and fiber_state_manager_ at first PushTask, but actor tasks now enqueue on io_service_ immediately while creation still runs on task_execution_service_. Pipelined arg completion can dispatch execution before creation installs real fiber/pool managers, leaving asyncio actors with a null fiber_state_manager_ or wrong concurrency settings permanently.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 39072cc. Configure here.

namespace ray {
namespace core {

/// Handle for posting a piece of work onto an execution context

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is something similar in gcs:

class Postable;

Should see if it makes sense to unify

RAY_CHECK(op_status == boost::fibers::channel_op_status::success);
}

void Post(std::function<void()> fn) override { EnqueueFiber(std::move(fn)); }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to keep EnqueueFiber around instead of just renaming to Post?

@edoakes

edoakes commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

LGTM, but serve tests still failing which is concerning

karticam added 7 commits June 21, 2026 20:54
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
karticam added 3 commits June 21, 2026 20:54
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
@karticam
karticam force-pushed the karticam/fix-actor-arg-fetch-pipelining-v2 branch from 3ad7ff5 to 464d288 Compare June 22, 2026 03:54
Signed-off-by: Kartica Modi <karticamodi@gmail.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 5a8b83d. Configure here.

},
"CoreWorker.MarkActorTaskArgsReady");
RAY_LOG(DEBUG) << "Actor task args are ready for tag: " << request.tag();
actor_task_execution_arg_waiter_->MarkReady(request.tag());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Late arg wait after fiber Stop

High Severity

For asyncio actors, shutdown now runs task_receiver_->Stop() on io_service_ and closes the fiber channel while HandleActorCallArgWaitComplete still calls MarkReady inline with no shutdown guard. Pipelined arg prefetch leaves more in-flight waits; a late completion can still dispatch execution via EnqueueFiber on a closed channel and hit a fatal RAY_CHECK.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5a8b83d. Configure here.

robertnishihara added a commit to robertnishihara/ray that referenced this pull request Jun 25, 2026
Include <Windows.h> before boost/asio in thread_pool.h to reproduce
the WinSock.h / WinSock2.h header conflict on Windows builds. This
mirrors the issue seen in PR ray-project#63765 where header ordering in
task_execution files caused the same compilation failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Robert Nishihara <rkn@anyscale.com>
robertnishihara added a commit that referenced this pull request Jun 26, 2026
Compilation errors can happen if `Windows.h` gets included before
`Boost.Asio` headers (this causes `WinSock.h`, which is included by
`Windows.h`, to conflict with `WinSock2.h`, which is included by
`Boost.Asio`).

The current solution is to define `WIN32_LEAN_AND_MEAN` in a bunch of
files. However, this is a bit fragile (and I noticed a compilation error
in #63765 caused by this).

Separately, I verified this in
#64331 (the first commit
triggers the compilation error, the second resolves it by setting the
global compiler flag).

The goal of this PR is to solve the compilation issue more globally.

This PR does two things
- Add `-DWIN32_LEAN_AND_MEAN` to the Windows compiler flags. This
prevents Windows.h from including WinSock.h, which conflicts with the
WinSock2.h required by Boost.Asio. It seems pretty normal to set this as
a global compiler flag. We already do this for hiredis.
- Remove redundant include guards and definitions of
`WIN32_LEAN_AND_MEAN` from files.

Note that we continue to define it in cpp/include/ray/api/logging.h
because in theory that file can be consumed externally without the
compiler flags.

Some details on what `WIN32_LEAN_AND_MEAN` excludes are in this post
https://stackoverflow.com/questions/11040133/what-does-defining-win32-lean-and-mean-exclude-exactly

---------

Signed-off-by: Robert Nishihara <rkn@anyscale.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
limarkdcunha pushed a commit to limarkdcunha/ray that referenced this pull request Jun 30, 2026
…oject#64361)

Compilation errors can happen if `Windows.h` gets included before
`Boost.Asio` headers (this causes `WinSock.h`, which is included by
`Windows.h`, to conflict with `WinSock2.h`, which is included by
`Boost.Asio`).

The current solution is to define `WIN32_LEAN_AND_MEAN` in a bunch of
files. However, this is a bit fragile (and I noticed a compilation error
in ray-project#63765 caused by this).

Separately, I verified this in
ray-project#64331 (the first commit
triggers the compilation error, the second resolves it by setting the
global compiler flag).

The goal of this PR is to solve the compilation issue more globally.

This PR does two things
- Add `-DWIN32_LEAN_AND_MEAN` to the Windows compiler flags. This
prevents Windows.h from including WinSock.h, which conflicts with the
WinSock2.h required by Boost.Asio. It seems pretty normal to set this as
a global compiler flag. We already do this for hiredis.
- Remove redundant include guards and definitions of
`WIN32_LEAN_AND_MEAN` from files.

Note that we continue to define it in cpp/include/ray/api/logging.h
because in theory that file can be consumed externally without the
compiler flags.

Some details on what `WIN32_LEAN_AND_MEAN` excludes are in this post
https://stackoverflow.com/questions/11040133/what-does-defining-win32-lean-and-mean-exclude-exactly

---------

Signed-off-by: Robert Nishihara <rkn@anyscale.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has not had
any activity for 14 days. It will be closed in another 14 days if no further activity occurs.
Thank you for your contributions.

You can always ask for help on our discussion forum or Ray's public slack channel.

If you'd like to keep this open, just leave any comment, and the stale label will be removed.

@github-actions github-actions Bot added the stale The issue is stale. It will be closed within 7 days unless there are further conversation label Jul 7, 2026
@karticam karticam changed the title [core] Fix actor arg fetch pipelining v2 [DO NOT MERGE][[core] Fix actor arg fetch pipelining v2 Jul 7, 2026
@karticam karticam changed the title [DO NOT MERGE][[core] Fix actor arg fetch pipelining v2 [DO NOT MERGE][core] Fix actor arg fetch pipelining v2 Jul 7, 2026
@github-actions github-actions Bot added unstale A PR that has been marked unstale. It will not get marked stale again if this label is on it. and removed stale The issue is stale. It will be closed within 7 days unless there are further conversation labels Jul 8, 2026
edoakes added a commit that referenced this pull request Jul 9, 2026
#63079)

Issue: The current design for actors arg fetch doesn't pipeline fetching
args when there is an actor task running on `task_execution_service`,
even though arg pulling is done by the raylet and not the core_worker.
This is because the IPC call made by the `core_worker` to the `raylet`
to start pulling args is posted on the `task_execution_service`.
Therefore, if a task is running, the IPC call can't be made and thus,
arg pulling is not done.

Fix: To fix this, we trigger the raylet IPC call from the `io_service`
and then queue the task execution on `task_execution_service`. This PR
breaks down the `ActorTaskExecutionArgWaiter` API into three parts -
1. `BeginArgsFetch`: Make the raylet IPC call and get a tag for this
call.
2. `OnArgsReady`: Register a callback which will be triggered when args
are ready for this task (identified by the tag returned in 1.).
3. `MarkReady`: Fire the registered callback when args are ready.

2 & 3 are done on the `task_execution_service`. In most cases, 2 would
always be called before 3 by virtue of `task_execution_service` being
FIFO. HandlePushTask (which queues 2) would be finished to completion
before `HandleActorCallArgWaitComplete` could run and post 3 on
`task_execution_service`. For the cases, where this doesn't hold true
(for eg: unordered actors where some tasks are executed later), and
`MarkReady` is called before the callback is registered, in
`OnArgsReady` the callback is executed synchronously.

Results: 
I tested the change against a ray script which spins up a producer actor
and a consumer actor on different nodes. The producer produces large
objects which are consumed by the consumer triggering plasma object
pulls. The consumer is slow (sleeps infinitely). Without the fix, the
consumer pulls some objects, but as soon as it starts executing the task
(which sleeps infinitely), object pulling stops and as a result, only
some of the objects are pulled from the producer side. With the fix, all
objects are pulled from producer.

Observe the object store memory column below (each object is 128MB)

<img width="1580" height="723" alt="image"
src="https://github.com/user-attachments/assets/4b8e28b0-6cd1-487b-996f-645a3b321edd"
/>
Producer produced 10 objects, consumer pulled only 3 (buggy case)

<img width="1578" height="726" alt="image"
src="https://github.com/user-attachments/assets/02af1966-781a-4187-8bf7-e23d41d375c0"
/>
Producer produced 10 objects, consumer pulled all (fixed case)




NOTES: 
1. Another caveat with this is that we might run into OOMs or object
spillings if a large number of tasks are submitted to actors with
significant argument size since we are fetching args right on
`HandlePushTask`. However, in the existing design too, this guarantee
isn't provided since it also triggers arg fetch for all tasks until the
time first arg fetch is complete and the task starts executing. In the
worst case, it could also trigger arg fetches enough to OOM if the first
arg fetch takes long enough.

EDIT:
Another solution was considered as per some suggestions in this PR was
to restructure the code so that only the user task body
(request.Execute()) runs on task_execution_service_ (or a
concurrency-group pool / asyncio fiber). Everything else — queue
bookkeeping, args-fetch IPC dispatch, cancellation decisions, dependency
resolution, timer management — runs on io_service_. Here is the PR for
that approach: #63765

This led to several issues with async actors which were hard to debug: 
1. Serve tests started to fail - `EnqueueFiber` which pushes the
function to an unbuffered channel blocks, and since the operation is
done on `io_service_`, this results in health-check RPC failing and the
ServeController force-killed healthy replicas.
2. `test_async_actor_cancel`: Became flaky due to running cancellation
path on `io_service_`.
3. `test_asyncio_actor_shutdown_when_non_async_method_mixed` started to
fail as well.
 
Due to this, we have decided to go ahead with the targeted approach in
this PR for now.

---------

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Co-authored-by: Edward Oakes <ed.nmi.oakes@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Issues that should be addressed in Ray Core go add ONLY when ready to merge, run all tests unstale A PR that has been marked unstale. It will not get marked stale again if this label is on it.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants