[core][joblib] Support adjustable actor capacity in ray.util.multiprocessing.Pool - #64957
[core][joblib] Support adjustable actor capacity in ray.util.multiprocessing.Pool#64957OneSizeFitsQuorum wants to merge 27 commits into
Conversation
fb9e1bd to
f32d423
Compare
There was a problem hiding this comment.
Code Review
This pull request introduces an elastic Ray task backend ("ray_tasks") for joblib, allowing each batch to run as a short-lived Ray task instead of using a fixed-size actor pool. This enables better integration with the Ray autoscaler and allows idle workers to scale down. The changes include documentation, unit tests, and the backend implementation itself. Feedback focuses on ensuring thread safety when modifying the in-flight task list concurrently from background threads, wrapping the RayBatchedCalls import in a try-except block to handle potential import failures, and making effective_n_jobs more robust when Ray is not yet initialized or when CPU resources are missing from the cluster state.
|
@OneSizeFitsQuorum do you use the joblib backend? |
|
@edoakes Yes, I’m actively using the joblib backend. My goal is to take tasks that are currently parallelized locally with joblib and scale them out to a Kubernetes-backed Ray cluster by allowing the worker pool to grow with demand. I found that the current pool cannot scale elastically on Kubernetes, which led me to investigate how this could be improved. I agree that making the actor pool autoscale would be preferable if we can retain a single backend. However, the current pool uses fixed I’m happy to explore an autoscaling actor pool, potentially as an opt-in mode first. Would you prefer implementing it in |
I would suggest prototyping/validating it in whatever way is easiest. Could be in the multiprocessing pool, actor pool, or just hand-written. Once you have validated the behavior/performance, we can decide the best abstraction & layering. Off the top of my head, I would think it probably makes sense to have the actor pool support autoscaling optionally (and use it for the joblib backend), and then the multiprocessing pool could just be a fixed-size actor pool. |
|
@edoakes Thanks for the idea! I'll test it out and evaluate it. |
5a910c4 to
8be4a0e
Compare
cae607c to
fb80fa9
Compare
There was a problem hiding this comment.
Code Review
This pull request introduces an experimental opt-in autoscaling mode (autoscale=True) for the Ray joblib backend and ray.util.multiprocessing.Pool, allowing the actor pool to grow on demand and shrink when idle. The feedback highlights several critical issues in the implementation: a strong reference cycle in the background reaper thread that prevents garbage collection of the Pool instance, a bug in task count tracking when replacing actors that reach maxtasksperchild, and a race condition where active actors executing long-running tasks could be prematurely reaped. To address these, it is recommended to use a static method with a weak reference for the reaper thread and to track active object references to ensure only truly idle actors are reaped.
Additional adversarial validationI ran two manual adversarial test matrices against the latest pull-based Local Ray state-machine matrixEnvironment: Python 3.11, local Ray cluster with 2 CPUs, using the clean
The temporary adversarial harness ran 10 test instances successfully after This matrix found two related iterator lifecycle bugs:
The final implementation sends the known final chunk count together with a KubeRay control-plane matrixEnvironment:
Observed Kubernetes lifecycle: Two deployment observations are worth calling out:
No additional Pool correctness issue was found in the KubeRay matrix. |
3b17124 to
d6f4e68
Compare
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
|
@edoakes @robertnishihara This PR is now ready for review. After several iterations, adjustable actor capacity is now integrated as a native part of I’ve also put significant effort into simplifying the implementation, organizing the main path ahead of compatibility handling, and keeping the code and documentation readable. Finally, the behavior has been validated across Pool and Joblib integration scenarios, including scaling, idle retirement, pending-resource handoff, actor recycling, failures, and compatibility. I’d appreciate another review when you have time. Thanks! |
There was a problem hiding this comment.
Code Review
This pull request introduces dynamic actor capacity management (autoscaling) for the Ray backend in Joblib and multiprocessing.Pool, allowing the pool to scale actors between min_size and max_size and release idle actors after idle_timeout_s. The feedback highlights critical deadlock risks where self._pool_lock is held during blocking submit operations, which would prevent closing or terminating the pool. Additionally, the reviewer suggests using isinstance() instead of direct type comparisons for PEP 8 compliance, and warns that treating transient RayActorErrors as terminal could permanently fail the pool unnecessarily.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
Reviewed by Cursor Bugbot for commit 2147276. Configure here.
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>
Signed-off-by: OneSizeFitsQuorum <tanxinyu@apache.org>

Summary
Closes #31128.
ray.util.multiprocessing.Poolpreviously fixed its actor count at construction time (defaulting to the CPUs then visible to Ray). This prevented a Pool from releasing actors after a burst and, in configurations such as a zero-CPU head node, from expressing pending actor demand to the Ray cluster autoscaler.This PR adds opt-in adjustable capacity between
min_sizeandmax_size. Supplying any capacity option selects the adjustable scheduler. A Pool without capacity options keeps the previous fixed scheduler and createsprocessesactors, preserving the existing default path. A lowermin_sizeallows actors above that floor to be released afteridle_timeout_s:The Ray Joblib backend forwards the same capacity options and
maxtasksperchild. Joblib'sn_jobsremains the concurrency ceiling, somax_sizemay lower but never raise it.Pool capacity and cluster autoscaling remain separate. Actor resource requirements do not change implicitly: callers use
ray_remote_argsto request CPUs, GPUs, or custom resources when pending actors should create cluster demand.Design
There are two deliberately separate scheduling paths:
This keeps the common fixed Pool behavior unchanged and limits the new lifecycle machinery to callers that opt into elasticity. Non-default
max_concurrency,max_restarts,max_task_retries, andget_if_existsremain supported by the fixed scheduler. They are rejected when combined with adjustable capacity because those Ray actor policies make bounded physical actor ownership ambiguous.For adjustable pools, Ray actor mailboxes remain the task queue and ObjectRefs remain the result protocol. Batches are submitted directly to actors, including actors that are waiting for resources or running their initializer. There is no driver-side task dispatcher, task migration, recovery poller, operation registry, or batch replay. The only new background thread retires idle actors.
Each adjustable slot follows this lifecycle:
stateDiagram-v2 [*] --> EMPTY EMPTY --> STARTING: create actor STARTING --> ACTIVE: readiness succeeds STARTING --> DRAINING: close or task limit ACTIVE --> DRAINING: close, task limit, idle timeout, or resource handoff DRAINING --> EMPTY: actor exit confirmed STARTING --> EMPTY: actor death confirmed ACTIVE --> EMPTY: actor death confirmedA slot owns its actor until Ray confirms the actor has exited.
STARTINGactors may already have accepted mailbox work, whileACTIVEonly means readiness has been observed. Generation checks prevent delayed callbacks from modifying a later actor that reuses the same slot.The reaper and readiness, batch, and termination ObjectRef callbacks observe the actor set through weak references. A callback retains only a slot index and generation—not the slot or actor handle—then resolves the current slot after confirming the actor set still exists. An abandoned Pool therefore does not remain alive solely because an actor is permanently pending.
An idle
ACTIVEactor normally waits foridle_timeout_sbefore retirement. The one normal exception is resource handoff: when aSTARTINGactor already owns accepted work but cannot acquire resources, anACTIVEactor with no outstanding work drains immediately. Because accepted mailbox work cannot migrate between actors, this releases Pool resources that might otherwise strand the pending work until the idle timeout. The capacity floor is rechecked before each such retirement.The scheduler prefers an already-ready actor while it is at most one batch more loaded than a
STARTINGactor. This intentionally avoids imposing actor startup latency on small tasks; a short warm burst can consequently use less than the eventual maximum parallelism.maxtasksperchildcounts accepted batches. Once an actor reaches the limit, its slot stops accepting work, drains accepted batches, and becomes reusable only after actor-exit confirmation.The Joblib backend constructs the Ray Pool directly instead of mutating Joblib's process-global
PicklingPoolbases. It owns backend arguments instead of relying on version-specific Joblib base-class storage, then merges them with arguments supplied toconfigure(). Pool options such asinitializer,initargs,ray_address, capacity settings, andmaxtasksperchildtherefore reach the Ray Pool on both Joblib 1.2 and 1.5.Failure semantics and lifecycle boundaries
close()rejects new submissions and preserves accepted work;terminate()may abort accepted work;join()waits for actor cleanup.ActorDiedErrorreleases the slot so capacity can be restored when policy requires it.ActorUnavailableErroris local to the affected ObjectRef and therefore fails that result or Joblib batch. It does not prove that the actor exited, so the slot retains the original handle and is not reused; it also does not permanently poison the Pool. A later call may succeed, report unavailability again, or confirm actor death.Parallelcall aborts and reports the error. Any Ray-level replay requires an explicit non-defaultmax_task_retries, which remains on the fixed scheduler rather than adjustable capacity.map_async()call, is not a distributed transaction. Earlier chunks may already be accepted if a later submission fails.join().Validation
The focused autoscaling suite contains 29 test functions (39 parameterized cases) covering:
maxtasksperchildrecycling;The latest local source-overlay validation used an installed Ray wheel for compiled extensions and the PR's Python modules and tests. The focused suite was invoked with
--noconftestand an equivalent localshutdown_onlyfixture because the checkout's repository-wide conftest imports build-only modules that do not match the installed wheel. The complete suite passed with the repository-pinned Joblib 1.2.0; the Joblib integration regressions also passed with Joblib 1.5.3:The wheel supplied
_rayletand other compiled extensions while the workspace supplied the changed pure-Python modules. Built-source CI remains authoritative for repository-wide and documentation integration.Duplicate-work check
Issue #31128 and open PRs in the Joblib/Pool autoscaling area were checked. No competing open implementation was found.
AI assistance and human accountability
AI assistance was used to develop, review, test, and document this change.
These items intentionally remain unchecked for the human submitter to complete.