-
Notifications
You must be signed in to change notification settings - Fork 38
REP: Ray Batch Actor Scheduling #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| # Summary | ||
|
|
||
| ### General Motivation | ||
|
|
||
| Ray currently processes actor creation requests one by one. For workloads that spawn hundreds or thousands of actors (e.g., large-scale RL), this sequential creation can become a bottleneck because each actor registration requires a separate RPC call to the GCS server, and the scheduling of actors may be suboptimal when evaluated on a per-actor basis. | ||
|
|
||
| This REP introduces a `ray.batch()` API, which provides a native context manager for scheduling Ray actors in batches. When actors are created inside the context block, Ray buffers the actor creation requests and processes them in a single RPC to the GCS server. By batching actor creation requests, we introduce opportunities to reduce scheduling overhead and optimize placement and resource allocation. | ||
|
|
||
|
|
||
| ### Should this change be within `ray` or outside? | ||
|
|
||
| Within Ray. | ||
|
|
||
| ## Stewardship | ||
|
|
||
| ### Required Reviewers | ||
|
|
||
| @MengjinYan | ||
| @Yicheng-Lu-llll | ||
|
|
||
| ### Shepherd of the Proposal (should be a senior committer) | ||
|
|
||
| @edoakes | ||
|
|
||
| ## Design and Architecture | ||
|
|
||
| ### API | ||
|
|
||
| The proposal introduces a new Ray Core API to schedule actors in batches using a context manager. See the example below: | ||
|
|
||
| ```python | ||
| import ray | ||
|
|
||
| # dummy class | ||
| @ray.remote | ||
| class MyActor: | ||
| def __init__(self): | ||
| pass | ||
|
|
||
| ray.init() | ||
|
|
||
| # create 1000 actors in a batch | ||
| with ray.batch(): | ||
| actors = [MyActor.remote() for _ in range(1000)] | ||
| ``` | ||
|
|
||
| ### Implementation Details | ||
|
|
||
| Under the hood, the batch context manager will signal the Core Worker to buffer actor creation requests instead of sending them to the GCS immediately. This allows the actor scheduler to optimize actor placement for co-location and locality, reducing overhead and improving overall scheduling throughput. | ||
|
|
||
| To achieve this, we extend the actor creation workflow as follows: | ||
|
|
||
| * **ray.batch() API**: The `ray.batch()` context manager signals to the Core Worker (e.g. Cython CoreWorkerProcess.GetCoreWorker().EnterActorBatch()) to enter batching mode for the current thread. Upon exit, it calls ExitActorBatch(). | ||
| * **Core Worker Actor Buffering**: Inside CoreWorker::CreateActor, if "batch mode" is enabled, the TaskSpecification for actors are pushed into a thread local buffer. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Brainstorming here: what happens if actors need dependencies? Some actors' dependencies get resolved quickly, while others resolve very slowly (waiting for an upstream task to produce some object). This dependency requirement might be unusual for the RL case, but if it does happen, then for the actors still waiting on dependency resolution, we can only register them but cannot submit. For the ones whose dependencies are already resolved, we can submit. I'm wondering how we should handle this. Would we end up submitting one by one? It seems like we can only batch the Register phase but cannot control the Submission Execution, so my sense is that we should keep the Submission Execution as is.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or opportunistically batching |
||
| * ActorIDs are generated synchronously (as usual via the worker context deterministic generator) and returned to Python immediately, so the user has valid ActorHandles. | ||
| * When ExitActorBatch() is called, it flushes the buffer by invoking an RPC to the GCS server asynchronously to register all the actors in the buffer in one atomic operation (see RegisterActorBatchRequest below). | ||
| * **GCS RPC Extension (RegisterActorBatch)**: A new RPC RegisterActorBatchRequest will be added to gcs_service.proto. | ||
| * GcsActorManager implements HandleRegisterActorBatch, which iterates through the batched tasks and invokes RegisterActor for each task locally in the GCS. | ||
| * Once all tasks are successfully registered in the backend storage, a single RPC reply is returned to the Core Worker. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I remember correctly, for named actors, we don't return the actor handle unless the register response comes back. But for normal actors, we don't wait. My personal thinking is that the reason register phase exists, and we need to wait synchronously is we need to handle this case: if an actor is still waiting on dependency resolution and we do not register that actor first, then GCS does not know about this actor. But the actor handle may have already been given to others, and if the owner dies, those who hold the actor handles will wait forever. So it makes sense to me that we should synchronously wait for all register responses. But we need to understand why we don't wait for normal actors. Is this for performance reasons? Will we cause a regression here? |
||
| * **Submission Execution**: After the GCS responds to the RegisterActorBatch RPC, the Core Worker loops over the batch and calls actor_task_submitter_->SubmitActorCreationTask(task_spec) to push the tasks to the placement group / scheduling queue. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we also clarify the behavior when some of the actors get scheduled but the rest fail due to insufficient resources? Though I think the behavior would be the same. |
||
|
|
||
| ## Compatibility, Deprecation, and Migration Plan | ||
|
|
||
| This change introduces a new API (`ray.batch()`) and will not impact behavior of any | ||
| existing Ray API. No changes will be made to Ray's default actor scheduling. | ||
|
|
||
| ## Test Plan and Acceptance Criteria | ||
|
|
||
| * Compare the latency and throughput of creating 1,000 and 10,000 actors with and without `ray.batch()`. Acceptance criteria includes a significant reduction in GCS CPU utilization and lower end-to-end actor creation latency. | ||
| * Ensure actors function identically whether inside or outside `ray.batch()` context manager. | ||
| * Unit, integration, e2e tests for `ray.batch()` | ||
|
|
||
| ## Alternatives | ||
|
|
||
| ### Alternative 1: `MyActor.batch_remote(num, *args, **kwargs)` | ||
|
|
||
| Instead of a context manager, introduce a new remote actor method like `MyActor.batch_remote(num, *args, **kwargs)`. While this approach could work, a context manager | ||
| provides more flexibility for batching heterogeneous actors with different resources/arguments in the same batch. | ||
|
|
||
| ### Alternative 2: Auto-batching of Actors | ||
|
|
||
| Automatically buffer actor creation requests in the background for a short duration without an explicit API. This approach introduces non-deterministic latency to individual actor creations and introduces significant risk of breaking existing behavior for Ray actors. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's also worth mentioning that, particularly for named actors, we need to wait for the register RPC to come back before we can start the next one. Since the current call has to return first, this serialization ends up slowing everything down. Normal actors can do this concurrently. I would expect Batch really helps here.